diff options
Diffstat (limited to 'tests/sys')
409 files changed, 20205 insertions, 1299 deletions
diff --git a/tests/sys/Makefile b/tests/sys/Makefile index 20ea4f181c7c..535e627d22cb 100644 --- a/tests/sys/Makefile +++ b/tests/sys/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys @@ -7,6 +6,7 @@ TESTS_SUBDIRS+= acl TESTS_SUBDIRS+= aio TESTS_SUBDIRS+= ${_audit} TESTS_SUBDIRS+= auditpipe +TESTS_SUBDIRS+= cam TESTS_SUBDIRS+= capsicum TESTS_SUBDIRS+= ${_cddl} # XXX: Currently broken in CI @@ -31,6 +31,7 @@ TESTS_SUBDIRS+= netpfil TESTS_SUBDIRS+= opencrypto TESTS_SUBDIRS+= posixshm TESTS_SUBDIRS+= ses +TESTS_SUBDIRS+= sound TESTS_SUBDIRS+= sys TESTS_SUBDIRS+= vfs TESTS_SUBDIRS+= vm diff --git a/tests/sys/Makefile.inc b/tests/sys/Makefile.inc index 03efce15f856..cec69b26e149 100644 --- a/tests/sys/Makefile.inc +++ b/tests/sys/Makefile.inc @@ -1,2 +1 @@ - .include "${SRCTOP}/tests/Makefile.inc0" diff --git a/tests/sys/acl/Makefile b/tests/sys/acl/Makefile index e8db261edaec..0080e8dd5b5a 100644 --- a/tests/sys/acl/Makefile +++ b/tests/sys/acl/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/acl diff --git a/tests/sys/aio/Makefile b/tests/sys/aio/Makefile index b0bddb044268..5cddb28c27a6 100644 --- a/tests/sys/aio/Makefile +++ b/tests/sys/aio/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/aio ATF_TESTS_C+= aio_test diff --git a/tests/sys/aio/aio_kqueue_test.c b/tests/sys/aio/aio_kqueue_test.c index c2478a9d05b3..5e5cb40d0752 100644 --- a/tests/sys/aio/aio_kqueue_test.c +++ b/tests/sys/aio/aio_kqueue_test.c @@ -35,6 +35,7 @@ #include <sys/types.h> #include <sys/event.h> #include <sys/time.h> +#include <assert.h> #include <aio.h> #include <err.h> #include <errno.h> @@ -192,6 +193,7 @@ main (int argc, char *argv[]) for (j = 0; j < max_queue_per_proc && iocb[j] != kq_iocb; j++) ; + assert(j < max_queue_per_proc); #ifdef DEBUG printf("kq_iocb %p\n", kq_iocb); diff --git a/tests/sys/audit/Makefile b/tests/sys/audit/Makefile index c9068d0a8044..d6d9c2874d09 100644 --- a/tests/sys/audit/Makefile +++ b/tests/sys/audit/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/audit ATF_TESTS_C= file-attribute-access diff --git a/tests/sys/auditpipe/Makefile b/tests/sys/auditpipe/Makefile index 20832301f044..189535ee74ca 100644 --- a/tests/sys/auditpipe/Makefile +++ b/tests/sys/auditpipe/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/auditpipe ATF_TESTS_C= auditpipe_test diff --git a/tests/sys/cam/Makefile b/tests/sys/cam/Makefile new file mode 100644 index 000000000000..4cc36604280a --- /dev/null +++ b/tests/sys/cam/Makefile @@ -0,0 +1,7 @@ +.include <src.opts.mk> + +TESTSDIR= ${TESTSBASE}/sys/cam + +TESTS_SUBDIRS+= ctl + +.include <bsd.test.mk> diff --git a/tests/sys/cam/ctl/Makefile b/tests/sys/cam/ctl/Makefile new file mode 100644 index 000000000000..05f0831fc8b0 --- /dev/null +++ b/tests/sys/cam/ctl/Makefile @@ -0,0 +1,20 @@ +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/cam/ctl +BINDIR=${TESTSDIR} + +${PACKAGE}FILES+= ctl.subr + +ATF_TESTS_SH+= persist +ATF_TESTS_SH+= prevent +ATF_TESTS_SH+= read_buffer +ATF_TESTS_SH+= start_stop_unit + +PROGS+= prout_register_huge_cdb +LIBADD+= cam +CFLAGS+= -I${SRCTOP}/sys + +# Must be exclusive because it disables/enables camsim +TEST_METADATA+= is_exclusive="true" + +.include <bsd.test.mk> diff --git a/tests/sys/cam/ctl/ctl.subr b/tests/sys/cam/ctl/ctl.subr new file mode 100644 index 000000000000..5da441b806f0 --- /dev/null +++ b/tests/sys/cam/ctl/ctl.subr @@ -0,0 +1,115 @@ +# vim: filetype=sh + +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Axcient +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +load_modules() { + if ! kldstat -q -m ctl; then + kldload ctl || atf_skip "could not load ctl kernel mod" + fi + if ! ctladm port -o on -p 0; then + atf_skip "could not enable the camsim frontend" + fi +} + +find_device() { + LUN=$1 + + # Rescan camsim + # XXX camsim doesn't update when creating a new device. Worse, a + # rescan won't look for new devices. So we must disable/re-enable it. + # Worse still, enabling it isn't synchronous, so we need a retry loop + # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=281000 + retries=5 + ctladm port -o off -p 0 >/dev/null + ctladm port -o on -p 0 >/dev/null + HEXLUN=`printf %x $LUN` + while true; do + dev=`camcontrol devlist | awk -v lun=$HEXLUN ' + /FREEBSD CTL.*,pass/ && $9==lun { + split($10, fields, /[,]/); print fields[1]; + } + /FREEBSD CTL.*\(pass/ && $9==lun { + split($10, fields, /[,]/); print fields[2]; + } + ' | sed 's:[()]::'` + if [ -z "$dev" -o ! -c /dev/$dev ]; then + retries=$(( $retries - 1 )) + if [ $retries -eq 0 ]; then + cat lun-create.txt + camcontrol devlist + atf_fail "Could not find GEOM device" + fi + sleep 0.1 + continue + fi + break + done + # Ensure that it's actually ready. camcontrol may report the disk's + # ident before it's actually ready to receive commands. Maybe that's + # because all of the GEOM providers must probe it? + while true; do + dd if=/dev/$dev bs=4096 count=1 of=/dev/null >/dev/null 2>/dev/null && break + retries=$(( $retries - 1 )) + if [ $retries -eq 0 ]; then + atf_fail "Device never became ready" + fi + sleep 0.1 + done +} + +# Create a CTL LUN backed by a file +create_block() { + EXTRA_ARGS=$* + + atf_check -o save:lun-create.txt ctladm create -b block $EXTRA_ARGS + atf_check egrep -q "LUN created successfully" lun-create.txt + LUN=`awk '/LUN ID:/ {print $NF}' lun-create.txt` + if [ -z "$LUN" ]; then + atf_fail "Could not find LUN id" + fi + find_device $LUN +} + +# Create a CTL LUN backed by RAM +create_ramdisk() { + EXTRA_ARGS=$* + + atf_check -o save:lun-create.txt ctladm create -b ramdisk -s 1048576 $EXTRA_ARGS + atf_check egrep -q "LUN created successfully" lun-create.txt + LUN=`awk '/LUN ID:/ {print $NF}' lun-create.txt` + if [ -z "$LUN" ]; then + atf_fail "Could not find LUN id" + fi + find_device $LUN +} + +cleanup() { + if [ -e "lun-create.txt" ]; then + backend=`awk '/backend:/ {print $NF}' lun-create.txt` + lun_id=`awk '/LUN ID:/ {print $NF}' lun-create.txt` + ctladm remove -b $backend -l $lun_id > /dev/null + fi +} diff --git a/tests/sys/cam/ctl/persist.sh b/tests/sys/cam/ctl/persist.sh new file mode 100644 index 000000000000..2a350ee4775a --- /dev/null +++ b/tests/sys/cam/ctl/persist.sh @@ -0,0 +1,349 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 ConnectWise +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +. $(atf_get_srcdir)/ctl.subr + +# TODO +# * PRIN READ RESERVATION, with one reservation +# * PROUT with illegal type +# * PROUT REGISTER AND IGNORE EXISTING KEY +# * PROUT REGISTER AND IGNORE EXISTING KEY with a RESERVATION KEY that isn't registered +# * PROUT REGISTER AND IGNORE EXISTING KEY to unregister +# * PROUT CLEAR allows previously prevented medium removal +# * PROUT PREEMPT +# * PROUT PREEMPT with a RESERVATION KEY that isn't registered +# * PROUT PREEMPT_AND_ABORT +# * PROUT PREEMPT_AND_ABORT with a RESERVATION KEY that isn't registered +# * PROUT REGISTER AND MOVE +# * PROUT REGISTER AND MOVE with a RESERVATION KEY that isn't registered +# * multiple initiators + +# Not Tested +# * PROUT REPLACE LOST RESERVATION (not supported by ctl) +# * Specify Initiator Ports bit (not supported by ctl) +# * Activate Persist Through Power Loss bit (not supported by ctl) +# * All Target Ports bit (not supported by ctl) + +RESERVATION_KEY=0xdeadbeef1a7ebabe + +atf_test_case prin_read_full_status_empty cleanup +prin_read_full_status_empty_head() +{ + atf_set "descr" "PERSISTENT RESERVATION IN with the READ FULL STATUS service action, with no status descriptors" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prin_read_full_status_empty_body() +{ + create_ramdisk + + atf_check -o match:"No full status descriptors" sg_persist -ns /dev/$dev +} +prin_read_full_status_empty_cleanup() +{ + cleanup +} + +atf_test_case prin_read_keys_empty cleanup +prin_read_keys_empty_head() +{ + atf_set "descr" "PERSISTENT RESERVATION IN with the READ KEYS service action, with no registered keys" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prin_read_keys_empty_body() +{ + create_ramdisk + + atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev +} +prin_read_keys_empty_cleanup() +{ + cleanup +} + +atf_test_case prin_read_reservation_empty cleanup +prin_read_reservation_empty_head() +{ + atf_set "descr" "PERSISTENT RESERVATION IN with the READ RESERVATION service action, with no reservations" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prin_read_reservation_empty_body() +{ + create_ramdisk + + atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev +} +prin_read_reservation_empty_cleanup() +{ + cleanup +} + +atf_test_case prin_report_capabilities cleanup +prin_report_capabilities_head() +{ + atf_set "descr" "PERSISTENT RESERVATION IN with the REPORT CAPABILITIES service action" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prin_report_capabilities_body() +{ + create_ramdisk + + cat > expected <<HERE +Report capabilities response: + Replace Lost Reservation Capable(RLR_C): 0 + Compatible Reservation Handling(CRH): 1 + Specify Initiator Ports Capable(SIP_C): 0 + All Target Ports Capable(ATP_C): 0 + Persist Through Power Loss Capable(PTPL_C): 0 + Type Mask Valid(TMV): 1 + Allow Commands: 5 + Persist Through Power Loss Active(PTPL_A): 0 + Support indicated in Type mask: + Write Exclusive, all registrants: 1 + Exclusive Access, registrants only: 1 + Write Exclusive, registrants only: 1 + Exclusive Access: 1 + Write Exclusive: 1 + Exclusive Access, all registrants: 1 +HERE + atf_check -o file:expected sg_persist -nc /dev/$dev +} +prin_report_capabilities_cleanup() +{ + cleanup +} + +atf_test_case prout_clear cleanup +prout_clear_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT with the CLEAR service action" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_clear_body() +{ + create_ramdisk + + # First register a key + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + + # Then make a reservation using that key + atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev + + # Now, clear all reservations and registrations + atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --clear /dev/$dev + + # Finally, check that all reservations and keys are gone + atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev + atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev +} +prout_clear_cleanup() +{ + cleanup +} + + +atf_test_case prout_register cleanup +prout_register_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT with the REGISTER service action" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_register_body() +{ + create_ramdisk + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev +} +prout_register_cleanup() +{ + cleanup +} + +atf_test_case prout_register_duplicate cleanup +prout_register_duplicate_head() +{ + atf_set "descr" "attempting to register a key twice should fail" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_register_duplicate_body() +{ + create_ramdisk + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + atf_check -s exit:24 -e match:"Reservation conflict" sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev +} +prout_register_duplicate_cleanup() +{ + cleanup +} + +atf_test_case prout_register_huge_cdb cleanup +prout_register_huge_cdb_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT with an enormous CDB size should not cause trouble" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_register_huge_cdb_body() +{ + create_ramdisk + + atf_check -s exit:1 $(atf_get_srcdir)/prout_register_huge_cdb $LUN +} +prout_register_huge_cdb_cleanup() +{ + cleanup +} + +atf_test_case prout_register_unregister cleanup +prout_register_unregister_head() +{ + atf_set "descr" "use PERSISTENT RESERVATION OUT with the REGISTER service action to remove a prior registration" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_register_unregister_body() +{ + create_ramdisk + # First register a key + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + # Then unregister it + atf_check sg_persist -n --out --param-sark=0 --param-rk=$RESERVATION_KEY -G /dev/$dev + # Finally, check that no keys are registered + atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev +} +prout_register_unregister_cleanup() +{ + cleanup +} + +atf_test_case prout_release cleanup +prout_release_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT with the RESERVE service action" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_release_body() +{ + create_ramdisk + + # First register a key + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + + # Then make a reservation using that key + atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev + atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --prout-type=8 --release /dev/$dev + + # Now check that the reservation is released + atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev + # But the registration shouldn't be. + atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev +} +prout_release_cleanup() +{ + cleanup +} + + +atf_test_case prout_reserve cleanup +prout_reserve_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT with the RESERVE service action" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist ctladm +} +prout_reserve_body() +{ + create_ramdisk + # First register a key + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + # Then make a reservation using that key + atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev + # Finally, check that the reservation is correct + cat > expected <<HERE + PR generation=0x1 + Key=0xdeadbeef1a7ebabe + All target ports bit clear + Relative port address: 0x0 + << Reservation holder >> + scope: LU_SCOPE, type: Exclusive Access, all registrants + Transport Id of initiator: + Parallel SCSI initiator SCSI address: 0x1 + relative port number (of corresponding target): 0x0 +HERE + atf_check -o file:expected sg_persist -ns /dev/$dev +} +prout_reserve_cleanup() +{ + cleanup +} + +atf_test_case prout_reserve_bad_scope cleanup +prout_reserve_bad_scope_head() +{ + atf_set "descr" "PERSISTENT RESERVATION OUT will be rejected with an unknown scope field" + atf_set "require.user" "root" + atf_set "require.progs" sg_persist camcontrol ctladm +} +prout_reserve_bad_scope_body() +{ + create_ramdisk + # First register a key + atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev + + # Then make a reservation using that key + atf_check -s exit:1 -e match:"ILLEGAL REQUEST asc:24,0 .Invalid field in CDB." camcontrol persist $dev -o reserve -k $RESERVATION_KEY -T read_shared -s 15 -v + + # Finally, check that nothing has been reserved + atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev +} +prout_reserve_bad_scope_cleanup() +{ + cleanup +} + + +atf_init_test_cases() +{ + atf_add_test_case prin_read_full_status_empty + atf_add_test_case prin_read_keys_empty + atf_add_test_case prin_read_reservation_empty + atf_add_test_case prin_report_capabilities + atf_add_test_case prout_clear + atf_add_test_case prout_register + atf_add_test_case prout_register_duplicate + atf_add_test_case prout_register_huge_cdb + atf_add_test_case prout_register_unregister + atf_add_test_case prout_release + atf_add_test_case prout_reserve + atf_add_test_case prout_reserve_bad_scope +} diff --git a/tests/sys/cam/ctl/prevent.sh b/tests/sys/cam/ctl/prevent.sh new file mode 100644 index 000000000000..315bedc39581 --- /dev/null +++ b/tests/sys/cam/ctl/prevent.sh @@ -0,0 +1,161 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Axcient +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +. $(atf_get_srcdir)/ctl.subr + +# TODO +# * multiple initiators may block removal + +# Not Tested +# * persistent removal (not implemented in CTL) + +atf_test_case allow cleanup +allow_head() +{ + atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL will prevent a CD from being ejected" + atf_set "require.user" "root" + atf_set "require.progs" "sg_prevent sg_start ctladm" +} +allow_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + atf_check sg_prevent --prevent 1 /dev/$dev + + # Now sg_start --eject should fail + atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev + + atf_check sg_prevent --allow /dev/$dev + + # Now sg_start --eject should work again + atf_check -s exit:0 sg_start --eject /dev/$dev +} +allow_cleanup() +{ + cleanup +} + +atf_test_case allow_idempotent cleanup +allow_idempotent_head() +{ + atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL is idempotent when run from the same initiator" + atf_set "require.user" "root" + atf_set "require.progs" "sg_prevent sg_start ctladm" +} +allow_idempotent_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + atf_check sg_prevent --allow /dev/$dev + atf_check sg_prevent --allow /dev/$dev + atf_check sg_prevent --prevent 1 /dev/$dev + + # Even though we ran --allow twice, a single --prevent command should + # suffice to prevent ejecting. Multiple ALLOW/PREVENT commands from + # the same initiator don't have any additional effect. + atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev +} +allow_idempotent_cleanup() +{ + cleanup +} + +atf_test_case nonremovable cleanup +nonremovable_head() +{ + atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL may not be used on non-removable media" + atf_set "require.user" "root" + atf_set "require.progs" "sg_prevent ctladm" +} +nonremovable_body() +{ + # Create a HDD, not a CD, device + create_ramdisk -t 0 + + atf_check -s exit:9 -e match:"Invalid opcode" sg_prevent /dev/$dev +} +nonremovable_cleanup() +{ + cleanup +} + +atf_test_case prevent cleanup +prevent_head() +{ + atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL will prevent a CD from being ejected" + atf_set "require.user" "root" + atf_set "require.progs" "sg_prevent sg_start ctladm" +} +prevent_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + atf_check sg_prevent --prevent 1 /dev/$dev + + # Now sg_start --eject should fail + atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev +} +prevent_cleanup() +{ + cleanup +} + +atf_test_case prevent_idempotent cleanup +prevent_idempotent_head() +{ + atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL is idempotent when run from the same initiator" + atf_set "require.user" "root" + atf_set "require.progs" "sg_prevent sg_start ctladm" +} +prevent_idempotent_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + atf_check sg_prevent --prevent 1 /dev/$dev + atf_check sg_prevent --prevent 1 /dev/$dev + atf_check sg_prevent --allow /dev/$dev + + # Even though we ran prevent idempotent and allow only once, eject + # should be allowed. Multiple PREVENT commands from the same initiator + # don't have any additional effect. + atf_check sg_start --eject /dev/$dev +} +prevent_idempotent_cleanup() +{ + cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case allow + atf_add_test_case allow_idempotent + atf_add_test_case nonremovable + atf_add_test_case prevent + atf_add_test_case prevent_idempotent +} diff --git a/tests/sys/cam/ctl/prout_register_huge_cdb.c b/tests/sys/cam/ctl/prout_register_huge_cdb.c new file mode 100644 index 000000000000..f57a6abfadd6 --- /dev/null +++ b/tests/sys/cam/ctl/prout_register_huge_cdb.c @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 ConnectWise + * All rights reserved. + * + * 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + */ + +/* + * Helper that sends a PERSISTENT RESERVATION OUT command to CTL with a + * ridiculously huge size for the length of the CDB. This is not possible with + * ctladm, for good reason. + */ +#include <camlib.h> +#include <err.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> + +#include <cam/scsi/scsi_message.h> +#include <cam/ctl/ctl_io.h> +#include <cam/ctl/ctl.h> +#include <cam/ctl/ctl_ioctl.h> +#include <cam/ctl/ctl_util.h> + +int +main(int argc, char **argv) +{ + union ctl_io *io; + int fd = open("/dev/cam/ctl", O_RDWR); + int r; + uint32_t targ_port; + + if (argc < 2) + errx(2, "usage: prout_register_huge_cdb <target_port>\n"); + + targ_port = strtoul(argv[1], NULL, 10); + + io = calloc(1, sizeof(*io)); + io->io_hdr.nexus.initid = 7; /* 7 is ctladm's default initiator id */ + io->io_hdr.nexus.targ_port = targ_port; + io->io_hdr.nexus.targ_mapped_lun = 0; + io->io_hdr.nexus.targ_lun = 0; + io->io_hdr.io_type = CTL_IO_SCSI; + io->taskio.tag_type = CTL_TAG_UNTAGGED; + uint8_t cdb[32] = {}; + // ctl_persistent_reserve_out// 5f 00 + cdb[0] = 0x5f; + cdb[1] = 0x00; + struct scsi_per_res_out *cdb_ = ( struct scsi_per_res_out *)cdb; + // Claim an enormous size of the CDB, but don't actually alloc it all. + cdb_->length[0] = 0xff; + cdb_->length[1] = 0xff; + cdb_->length[2] = 0xff; + cdb_->length[3] = 0xff; + io->scsiio.cdb_len = sizeof(cdb); + memcpy(io->scsiio.cdb, cdb, sizeof(cdb)); + io->io_hdr.flags |= CTL_FLAG_DATA_IN; + r = ioctl(fd, CTL_IO, io); + if (r == -1) + err(1, "ioctl"); + if ((io->io_hdr.status & CTL_STATUS_MASK) == CTL_SUCCESS) { + return (0); + } else { + return (1); + } +} diff --git a/tests/sys/cam/ctl/read_buffer.sh b/tests/sys/cam/ctl/read_buffer.sh new file mode 100644 index 000000000000..98515943dd40 --- /dev/null +++ b/tests/sys/cam/ctl/read_buffer.sh @@ -0,0 +1,172 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Axcient +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +# Not tested +# * modes other than "Data" and "Desc". We don't support those. +# * Buffer ID other than 0. We don't support those. +# * The Mode Specific field. We don't support it. + +. $(atf_get_srcdir)/ctl.subr + +atf_test_case basic cleanup +basic_head() +{ + atf_set "descr" "READ BUFFER can retrieve data previously written by WRITE BUFFER" + atf_set "require.user" "root" + atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm" +} +basic_body() +{ + create_ramdisk + + # Write to its buffer + cp /etc/passwd input + len=`wc -c input | cut -wf 2` + atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev + + # Read it back + atf_check -o save:output sg_read_buffer --mode data -l $len --raw /dev/$dev + + # And verify + if ! diff -q input output; then + atf_fail "Miscompare!" + fi +} +basic_cleanup() +{ + cleanup +} + +# Read from the Descriptor mode. Along with Data, these are the only two modes +# we support. +atf_test_case desc cleanup +desc_head() +{ + atf_set "descr" "READ BUFFER can retrieve the buffer size via the DESCRIPTOR mode" + atf_set "require.user" "root" + atf_set "require.progs" "sg_read_buffer ctladm" +} +desc_body() +{ + create_ramdisk + + atf_check -o inline:" 00 00 04 00 00\n" sg_read_buffer --hex --mode desc /dev/$dev +} +desc_cleanup() +{ + cleanup +} + +atf_test_case length cleanup +length_head() +{ + atf_set "descr" "READ BUFFER can limit its length with the LENGTH field" + atf_set "require.user" "root" + atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm" +} +length_body() +{ + create_ramdisk + + # Write to its buffer + atf_check -o ignore -e ignore dd if=/dev/random of=input bs=4096 count=1 + atf_check -o ignore -e ignore dd if=input bs=2048 count=1 of=expected + atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev + + # Read it back + atf_check -o save:output sg_read_buffer --mode data -l 2048 --raw /dev/$dev + + # And verify + if ! diff -q expected output; then + atf_fail "Miscompare!" + fi +} +length_cleanup() +{ + cleanup +} + +atf_test_case offset cleanup +offset_head() +{ + atf_set "descr" "READ BUFFER accepts the BUFFER OFFSET field" + atf_set "require.user" "root" + atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm" +} +offset_body() +{ + create_ramdisk + + # Write to its buffer + atf_check -o ignore -e ignore dd if=/dev/random of=input bs=4096 count=1 + atf_check -o ignore -e ignore dd if=input iseek=2 bs=512 count=1 of=expected + atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev + + # Read it back + atf_check -o save:output sg_read_buffer --mode data -l 512 -o 1024 --raw /dev/$dev + + # And verify + if ! diff -q expected output; then + atf_fail "Miscompare!" + fi +} +offset_cleanup() +{ + cleanup +} + +atf_test_case uninitialized cleanup +uninitialized_head() +{ + atf_set "descr" "READ BUFFER buffers are zero-initialized" + atf_set "require.user" "root" + atf_set "require.progs" "sg_read_buffer ctladm" +} +uninitialized_body() +{ + create_ramdisk + + # Read an uninitialized buffer + atf_check -o save:output sg_read_buffer --mode data -l 262144 --raw /dev/$dev + + # And verify + atf_check -o ignore -e ignore dd if=/dev/zero bs=262144 count=1 of=expected + if ! diff -q expected output; then + atf_fail "Miscompare!" + fi +} +uninitialized_cleanup() +{ + cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case basic + atf_add_test_case desc + atf_add_test_case length + atf_add_test_case offset + atf_add_test_case uninitialized +} diff --git a/tests/sys/cam/ctl/start_stop_unit.sh b/tests/sys/cam/ctl/start_stop_unit.sh new file mode 100644 index 000000000000..a1160b35e4a7 --- /dev/null +++ b/tests/sys/cam/ctl/start_stop_unit.sh @@ -0,0 +1,150 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Axcient +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +. $(atf_get_srcdir)/ctl.subr + +# TODO: +# * format layer +# * IMM bit +# * LOEJ +# * noflush +# * power conditions + +# Not Tested +# * Power Condition Modifier (not implemented in CTL) + +atf_test_case eject cleanup +eject_head() +{ + atf_set "descr" "START STOP UNIT can eject a CDROM device" + atf_set "require.user" "root" + atf_set "require.progs" "sg_start sg_readcap ctladm" +} +eject_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + # Verify that the device is online + # Too bad I don't know of any other way to check that it's stopped but + # by using sg_readcap. + atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev + + # eject the device + atf_check sg_start --eject /dev/$dev + + # Ejected, it should now return ENXIO + atf_check -s exit:1 -o ignore -e match:"Device not configured" dd if=/dev/$dev bs=4096 count=1 of=/dev/null +} +eject_cleanup() +{ + cleanup +} + +atf_test_case load cleanup +load_head() +{ + atf_set "descr" "START STOP UNIT can load a CDROM device" + atf_set "require.user" "root" + atf_set "require.progs" "sg_start sg_readcap ctladm" +} +load_body() +{ + # -t 5 for CD/DVD device type + create_ramdisk -t 5 + + # eject the device + atf_check sg_start --eject /dev/$dev + + # Verify that it's offline it should now return ENXIO + atf_check -s exit:1 -o ignore -e match:"Device not configured" dd if=/dev/$dev bs=4096 count=1 of=/dev/null + + # Load it again + atf_check sg_start --load /dev/$dev + + atf_check -o ignore -e ignore dd if=/dev/$dev bs=4096 count=1 of=/dev/null + atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev +} +load_cleanup() +{ + cleanup +} + +atf_test_case start cleanup +start_head() +{ + atf_set "descr" "START STOP UNIT can start a device" + atf_set "require.user" "root" + atf_set "require.progs" "sg_start sg_readcap ctladm" +} +start_body() +{ + create_ramdisk + + # stop the device + atf_check sg_start --stop /dev/$dev + + # And start it again + atf_check sg_start /dev/$dev + + # Now sg_readcap should succeed. Too bad I don't know of any other way + # to check that it's stopped. + atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev +} +start_cleanup() +{ + cleanup +} + +atf_test_case stop cleanup +stop_head() +{ + atf_set "descr" "START STOP UNIT can stop a device" + atf_set "require.user" "root" + atf_set "require.progs" "sg_start sg_readcap ctladm" +} +stop_body() +{ + create_ramdisk + + # Stop the device + atf_check sg_start --stop /dev/$dev + + # Now sg_readcap should fail. Too bad I don't know of any other way to + # check that it's stopped. + atf_check -s exit:2 -e match:"Device not ready" sg_readcap /dev/$dev +} +stop_cleanup() +{ + cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case eject + atf_add_test_case load + atf_add_test_case start + atf_add_test_case stop +} diff --git a/tests/sys/capsicum/Makefile b/tests/sys/capsicum/Makefile index 81cb4fa1ceee..fd8dcb29d65c 100644 --- a/tests/sys/capsicum/Makefile +++ b/tests/sys/capsicum/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys/capsicum @@ -14,6 +13,10 @@ CFLAGS+= -I${SRCTOP}/tests GTESTS+= capsicum-test GTESTS_WRAPPER_SH.capsicum-test= functional +# This test script runs the same test suite twice, once as root and once as an +# unprivileged user. Serialize them since some tests access global namespaces, +# e.g., mqueuefs, and can trample on each other. +TEST_METADATA.functional+= is_exclusive="true" SRCS.capsicum-test+= \ capsicum-test-main.cc \ diff --git a/tests/sys/cddl/Makefile b/tests/sys/cddl/Makefile index 80c72ea5ec42..66377e1e3bfd 100644 --- a/tests/sys/cddl/Makefile +++ b/tests/sys/cddl/Makefile @@ -1,11 +1,10 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys/cddl TESTS_SUBDIRS+= ${_zfs} -.if ${MK_ZFS} != "no" +.if ${MK_ZFS_TESTS} != "no" _zfs= zfs .endif diff --git a/tests/sys/cddl/zfs/Makefile b/tests/sys/cddl/zfs/Makefile index 18097f9b2617..f215f7438e78 100644 --- a/tests/sys/cddl/zfs/Makefile +++ b/tests/sys/cddl/zfs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys/cddl/zfs diff --git a/tests/sys/cddl/zfs/bin/Makefile b/tests/sys/cddl/zfs/bin/Makefile index 3dfb86b1bd5b..98db25e396d3 100644 --- a/tests/sys/cddl/zfs/bin/Makefile +++ b/tests/sys/cddl/zfs/bin/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/include/Makefile b/tests/sys/cddl/zfs/include/Makefile index 81e81cb2e0c7..c7712a7d0a88 100644 --- a/tests/sys/cddl/zfs/include/Makefile +++ b/tests/sys/cddl/zfs/include/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/tests/Makefile b/tests/sys/cddl/zfs/tests/Makefile index efaae7bfc2c5..f007e78992fc 100644 --- a/tests/sys/cddl/zfs/tests/Makefile +++ b/tests/sys/cddl/zfs/tests/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/tests/Makefile.inc b/tests/sys/cddl/zfs/tests/Makefile.inc index 069272ab8104..1b911c451c01 100644 --- a/tests/sys/cddl/zfs/tests/Makefile.inc +++ b/tests/sys/cddl/zfs/tests/Makefile.inc @@ -1,2 +1 @@ - WARNS?= 0 diff --git a/tests/sys/cddl/zfs/tests/acl/Makefile b/tests/sys/cddl/zfs/tests/acl/Makefile index 062a007a4096..95cbc40972e5 100644 --- a/tests/sys/cddl/zfs/tests/acl/Makefile +++ b/tests/sys/cddl/zfs/tests/acl/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/acl/cifs/Makefile b/tests/sys/cddl/zfs/tests/acl/cifs/Makefile index 30bfd2183a4d..bbb54429ec2c 100644 --- a/tests/sys/cddl/zfs/tests/acl/cifs/Makefile +++ b/tests/sys/cddl/zfs/tests/acl/cifs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile b/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile index f942fa2ace46..1332371f7570 100644 --- a/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile +++ b/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/acl/trivial/Makefile b/tests/sys/cddl/zfs/tests/acl/trivial/Makefile index ca54995a53a0..5a00bc05746b 100644 --- a/tests/sys/cddl/zfs/tests/acl/trivial/Makefile +++ b/tests/sys/cddl/zfs/tests/acl/trivial/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/atime/Makefile b/tests/sys/cddl/zfs/tests/atime/Makefile index 315123c59bbd..f89f32c149f3 100644 --- a/tests/sys/cddl/zfs/tests/atime/Makefile +++ b/tests/sys/cddl/zfs/tests/atime/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/bootfs/Makefile b/tests/sys/cddl/zfs/tests/bootfs/Makefile index f59b08261989..53e2224e1d8c 100644 --- a/tests/sys/cddl/zfs/tests/bootfs/Makefile +++ b/tests/sys/cddl/zfs/tests/bootfs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cache/Makefile b/tests/sys/cddl/zfs/tests/cache/Makefile index 1955009834fe..df46dedcc8ec 100644 --- a/tests/sys/cddl/zfs/tests/cache/Makefile +++ b/tests/sys/cddl/zfs/tests/cache/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cachefile/Makefile b/tests/sys/cddl/zfs/tests/cachefile/Makefile index 60bfda066a15..09b55f1681aa 100644 --- a/tests/sys/cddl/zfs/tests/cachefile/Makefile +++ b/tests/sys/cddl/zfs/tests/cachefile/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/clean_mirror/Makefile b/tests/sys/cddl/zfs/tests/clean_mirror/Makefile index a95d1a286df5..881d6cdedb7a 100644 --- a/tests/sys/cddl/zfs/tests/clean_mirror/Makefile +++ b/tests/sys/cddl/zfs/tests/clean_mirror/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/Makefile b/tests/sys/cddl/zfs/tests/cli_root/Makefile index 33b97022fc4f..05b2f480e006 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile index 33ae1fcdbdc4..7bf850245a84 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile index 814b790f1585..e123a67e482b 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile index ea8808442e1e..017e2c312ac6 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile index 6bb2031caf6b..f608eca9abf2 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile index 0a46a9e0b4c3..6f048fd37e7a 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile index 0aff64173c45..8e8777ff42af 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile index d2e61fdc8aae..2b6288c76ba8 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile index 2763a53db4ee..140e5634d23f 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile index 0949c0408257..3b532596cee1 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile index 4d06e3e082a8..b5b68263d8a5 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile index 94929d25dcf4..53c59cf0bd99 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile index ae621146fef3..91b14e1aeadd 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile index b71daeb93518..066039a3fa04 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile index 6794da9b054f..431ab61354b4 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile index eb1b17251ae6..07f9e88fe963 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile index f8426d398d79..d288ea35914b 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile index 6b505a3bee2a..ff100e37f330 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile index 10e68def5db2..69300727fd9e 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile index 29b992e97c2c..1027523ddcd6 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile index f1a7f4a4e7fa..7cad43c9d796 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile index 930191f2b7bb..ba37c8fc0f62 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile index e5d0ac041940..eeaceb593ef7 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile index a26461b62104..0b01b8f05aba 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile index fbbdb8c3dcf5..1d09a43ee977 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile index ed89d7ca995f..27986f8207fd 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile index 24ad0b17f737..d094cf65cbfd 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile index a40272300789..de2992fe86b3 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile index a331da6db05f..73026cf21ada 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile index a1be860244a7..90ceee214b00 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile index 7e4d63ea4c0c..353b8d22be79 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile index 0d1f98cab009..68265a18bbd0 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile index 5e87a94da1b7..fb15c0421a31 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile index b24ce38a3567..7066d97f02bc 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile index f2e930ad2fc1..c1341eaeec67 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile index a946dd6ceeba..508bfef28e4c 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile index 4f7e6e1a1054..13235edd1f47 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile index e1b55de436d8..22728ca40310 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile index 28ee77f2de35..73d3cb438b87 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile index 1de45591b801..39fdf272482f 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile index 00302f3833ef..dfe0a7c265d5 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile index fed1bd5a1094..5c855230c0d7 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile index 0477b0d99a64..4e8936cdb3b1 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile index 74b7a66e5e72..4340af728892 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile index 583d490387ab..f6365bbb46d5 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile index a31ad913b50c..2380e3be9e43 100644 --- a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_user/Makefile b/tests/sys/cddl/zfs/tests/cli_user/Makefile index 667d4133ae77..947337d58367 100644 --- a/tests/sys/cddl/zfs/tests/cli_user/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_user/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile b/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile index 85ff511360a9..d22caa7382c5 100644 --- a/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile index 5443ff4dec0d..14185efb74da 100644 --- a/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile index 4de5d3857568..4c66f6d8343a 100644 --- a/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile index d9f524c75d85..b50c82ccc1b4 100644 --- a/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile +++ b/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/compression/Makefile b/tests/sys/cddl/zfs/tests/compression/Makefile index 3ad018047e6e..3ed64fc3460e 100644 --- a/tests/sys/cddl/zfs/tests/compression/Makefile +++ b/tests/sys/cddl/zfs/tests/compression/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/ctime/Makefile b/tests/sys/cddl/zfs/tests/ctime/Makefile index 752ff9695a49..d7cf76d4517c 100644 --- a/tests/sys/cddl/zfs/tests/ctime/Makefile +++ b/tests/sys/cddl/zfs/tests/ctime/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/delegate/Makefile b/tests/sys/cddl/zfs/tests/delegate/Makefile index 5fb0be849309..c25841283c20 100644 --- a/tests/sys/cddl/zfs/tests/delegate/Makefile +++ b/tests/sys/cddl/zfs/tests/delegate/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/devices/Makefile b/tests/sys/cddl/zfs/tests/devices/Makefile index 4734f61096cf..6902f8c270be 100644 --- a/tests/sys/cddl/zfs/tests/devices/Makefile +++ b/tests/sys/cddl/zfs/tests/devices/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/exec/Makefile b/tests/sys/cddl/zfs/tests/exec/Makefile index 33dfcfad177b..2a8efb84b079 100644 --- a/tests/sys/cddl/zfs/tests/exec/Makefile +++ b/tests/sys/cddl/zfs/tests/exec/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/grow_pool/Makefile b/tests/sys/cddl/zfs/tests/grow_pool/Makefile index 9537fb62fde4..31fdf1fc9b39 100644 --- a/tests/sys/cddl/zfs/tests/grow_pool/Makefile +++ b/tests/sys/cddl/zfs/tests/grow_pool/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/grow_replicas/Makefile b/tests/sys/cddl/zfs/tests/grow_replicas/Makefile index 1eebf5ef0a31..06ee0c68c435 100644 --- a/tests/sys/cddl/zfs/tests/grow_replicas/Makefile +++ b/tests/sys/cddl/zfs/tests/grow_replicas/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/history/Makefile b/tests/sys/cddl/zfs/tests/history/Makefile index 8cd1babd3ecd..8a8175abbaea 100644 --- a/tests/sys/cddl/zfs/tests/history/Makefile +++ b/tests/sys/cddl/zfs/tests/history/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/hotplug/Makefile b/tests/sys/cddl/zfs/tests/hotplug/Makefile index 3da1b0f4174b..2264668f14bd 100644 --- a/tests/sys/cddl/zfs/tests/hotplug/Makefile +++ b/tests/sys/cddl/zfs/tests/hotplug/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/hotspare/Makefile b/tests/sys/cddl/zfs/tests/hotspare/Makefile index 9371186866b0..02a7aa3a1ac4 100644 --- a/tests/sys/cddl/zfs/tests/hotspare/Makefile +++ b/tests/sys/cddl/zfs/tests/hotspare/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/inheritance/Makefile b/tests/sys/cddl/zfs/tests/inheritance/Makefile index b337fd541ed2..8dc20cb7b3bd 100644 --- a/tests/sys/cddl/zfs/tests/inheritance/Makefile +++ b/tests/sys/cddl/zfs/tests/inheritance/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/interop/Makefile b/tests/sys/cddl/zfs/tests/interop/Makefile index e81bcb332628..cd26031d634d 100644 --- a/tests/sys/cddl/zfs/tests/interop/Makefile +++ b/tests/sys/cddl/zfs/tests/interop/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/inuse/Makefile b/tests/sys/cddl/zfs/tests/inuse/Makefile index eb26734de6a5..81405a420e98 100644 --- a/tests/sys/cddl/zfs/tests/inuse/Makefile +++ b/tests/sys/cddl/zfs/tests/inuse/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/iscsi/Makefile b/tests/sys/cddl/zfs/tests/iscsi/Makefile index 523e83904d37..c531c390e4fe 100644 --- a/tests/sys/cddl/zfs/tests/iscsi/Makefile +++ b/tests/sys/cddl/zfs/tests/iscsi/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/large_files/Makefile b/tests/sys/cddl/zfs/tests/large_files/Makefile index b169b19e6e13..e18bf056e290 100644 --- a/tests/sys/cddl/zfs/tests/large_files/Makefile +++ b/tests/sys/cddl/zfs/tests/large_files/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/largest_pool/Makefile b/tests/sys/cddl/zfs/tests/largest_pool/Makefile index 4a5fdcbaae54..12cb14f3a264 100644 --- a/tests/sys/cddl/zfs/tests/largest_pool/Makefile +++ b/tests/sys/cddl/zfs/tests/largest_pool/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/link_count/Makefile b/tests/sys/cddl/zfs/tests/link_count/Makefile index 8bbdc98f20a6..fd9aab99f355 100644 --- a/tests/sys/cddl/zfs/tests/link_count/Makefile +++ b/tests/sys/cddl/zfs/tests/link_count/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/migration/Makefile b/tests/sys/cddl/zfs/tests/migration/Makefile index dae2827fe2f2..68c9977d34b7 100644 --- a/tests/sys/cddl/zfs/tests/migration/Makefile +++ b/tests/sys/cddl/zfs/tests/migration/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/mmap/Makefile b/tests/sys/cddl/zfs/tests/mmap/Makefile index aeeecf79170a..494b84b8e155 100644 --- a/tests/sys/cddl/zfs/tests/mmap/Makefile +++ b/tests/sys/cddl/zfs/tests/mmap/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/mount/Makefile b/tests/sys/cddl/zfs/tests/mount/Makefile index fbbdc22506a0..6b00ecf96c1a 100644 --- a/tests/sys/cddl/zfs/tests/mount/Makefile +++ b/tests/sys/cddl/zfs/tests/mount/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/mv_files/Makefile b/tests/sys/cddl/zfs/tests/mv_files/Makefile index 8409af69bf58..b81bf3fa49a9 100644 --- a/tests/sys/cddl/zfs/tests/mv_files/Makefile +++ b/tests/sys/cddl/zfs/tests/mv_files/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/nestedfs/Makefile b/tests/sys/cddl/zfs/tests/nestedfs/Makefile index c15cdb402e56..0ffdee46247a 100644 --- a/tests/sys/cddl/zfs/tests/nestedfs/Makefile +++ b/tests/sys/cddl/zfs/tests/nestedfs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/no_space/Makefile b/tests/sys/cddl/zfs/tests/no_space/Makefile index 69aac3b4adba..db55c2674165 100644 --- a/tests/sys/cddl/zfs/tests/no_space/Makefile +++ b/tests/sys/cddl/zfs/tests/no_space/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/online_offline/Makefile b/tests/sys/cddl/zfs/tests/online_offline/Makefile index a66642a57d85..5f0e68a5ccd5 100644 --- a/tests/sys/cddl/zfs/tests/online_offline/Makefile +++ b/tests/sys/cddl/zfs/tests/online_offline/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/pool_names/Makefile b/tests/sys/cddl/zfs/tests/pool_names/Makefile index 040f9ebd46e0..34350e28a800 100644 --- a/tests/sys/cddl/zfs/tests/pool_names/Makefile +++ b/tests/sys/cddl/zfs/tests/pool_names/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/poolversion/Makefile b/tests/sys/cddl/zfs/tests/poolversion/Makefile index 45ddce8deec3..6c04aac0f5fb 100644 --- a/tests/sys/cddl/zfs/tests/poolversion/Makefile +++ b/tests/sys/cddl/zfs/tests/poolversion/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/quota/Makefile b/tests/sys/cddl/zfs/tests/quota/Makefile index 36784d03ae82..beae44a4a503 100644 --- a/tests/sys/cddl/zfs/tests/quota/Makefile +++ b/tests/sys/cddl/zfs/tests/quota/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/redundancy/Makefile b/tests/sys/cddl/zfs/tests/redundancy/Makefile index cef8705f12e7..30345b04cc78 100644 --- a/tests/sys/cddl/zfs/tests/redundancy/Makefile +++ b/tests/sys/cddl/zfs/tests/redundancy/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/refquota/Makefile b/tests/sys/cddl/zfs/tests/refquota/Makefile index 3b9caf86c059..0b86cf46eaf5 100644 --- a/tests/sys/cddl/zfs/tests/refquota/Makefile +++ b/tests/sys/cddl/zfs/tests/refquota/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/refreserv/Makefile b/tests/sys/cddl/zfs/tests/refreserv/Makefile index 0c04cf3284b3..983cc768a338 100644 --- a/tests/sys/cddl/zfs/tests/refreserv/Makefile +++ b/tests/sys/cddl/zfs/tests/refreserv/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/rename_dirs/Makefile b/tests/sys/cddl/zfs/tests/rename_dirs/Makefile index ce111219676d..9aafbf759374 100644 --- a/tests/sys/cddl/zfs/tests/rename_dirs/Makefile +++ b/tests/sys/cddl/zfs/tests/rename_dirs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/replacement/Makefile b/tests/sys/cddl/zfs/tests/replacement/Makefile index e172a1f6ac60..f818166cdaa8 100644 --- a/tests/sys/cddl/zfs/tests/replacement/Makefile +++ b/tests/sys/cddl/zfs/tests/replacement/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/reservation/Makefile b/tests/sys/cddl/zfs/tests/reservation/Makefile index 781146bd2418..cda06d9c96c8 100644 --- a/tests/sys/cddl/zfs/tests/reservation/Makefile +++ b/tests/sys/cddl/zfs/tests/reservation/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/tests/rootpool/Makefile b/tests/sys/cddl/zfs/tests/rootpool/Makefile index c82ea9ab2927..b4ddcc9b3256 100644 --- a/tests/sys/cddl/zfs/tests/rootpool/Makefile +++ b/tests/sys/cddl/zfs/tests/rootpool/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests diff --git a/tests/sys/cddl/zfs/tests/rsend/Makefile b/tests/sys/cddl/zfs/tests/rsend/Makefile index 2d459ea74c9e..708b53ed90e7 100644 --- a/tests/sys/cddl/zfs/tests/rsend/Makefile +++ b/tests/sys/cddl/zfs/tests/rsend/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile b/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile index ae8f222f16a5..504f6210f6a8 100644 --- a/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile +++ b/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/slog/Makefile b/tests/sys/cddl/zfs/tests/slog/Makefile index 69167acfc30c..0cdbca1395f3 100644 --- a/tests/sys/cddl/zfs/tests/slog/Makefile +++ b/tests/sys/cddl/zfs/tests/slog/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/snapshot/Makefile b/tests/sys/cddl/zfs/tests/snapshot/Makefile index 04275f3b2355..369dde3ba7ad 100644 --- a/tests/sys/cddl/zfs/tests/snapshot/Makefile +++ b/tests/sys/cddl/zfs/tests/snapshot/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/snapused/Makefile b/tests/sys/cddl/zfs/tests/snapused/Makefile index f65ecc1251a6..f831ac5dc699 100644 --- a/tests/sys/cddl/zfs/tests/snapused/Makefile +++ b/tests/sys/cddl/zfs/tests/snapused/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/sparse/Makefile b/tests/sys/cddl/zfs/tests/sparse/Makefile index df147d108320..c606a58e3b40 100644 --- a/tests/sys/cddl/zfs/tests/sparse/Makefile +++ b/tests/sys/cddl/zfs/tests/sparse/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/threadsappend/Makefile b/tests/sys/cddl/zfs/tests/threadsappend/Makefile index f62ad46f3777..fd179e27e715 100644 --- a/tests/sys/cddl/zfs/tests/threadsappend/Makefile +++ b/tests/sys/cddl/zfs/tests/threadsappend/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/truncate/Makefile b/tests/sys/cddl/zfs/tests/truncate/Makefile index 783456d96634..98c8e085408d 100644 --- a/tests/sys/cddl/zfs/tests/truncate/Makefile +++ b/tests/sys/cddl/zfs/tests/truncate/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/txg_integrity/Makefile b/tests/sys/cddl/zfs/tests/txg_integrity/Makefile index 010021572358..6a200b0c12b0 100644 --- a/tests/sys/cddl/zfs/tests/txg_integrity/Makefile +++ b/tests/sys/cddl/zfs/tests/txg_integrity/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/userquota/Makefile b/tests/sys/cddl/zfs/tests/userquota/Makefile index 6ec2a4f0b15c..678678ced1dd 100644 --- a/tests/sys/cddl/zfs/tests/userquota/Makefile +++ b/tests/sys/cddl/zfs/tests/userquota/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/utils_test/Makefile b/tests/sys/cddl/zfs/tests/utils_test/Makefile index 2b550b166a1f..e14bf4586580 100644 --- a/tests/sys/cddl/zfs/tests/utils_test/Makefile +++ b/tests/sys/cddl/zfs/tests/utils_test/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/write_dirs/Makefile b/tests/sys/cddl/zfs/tests/write_dirs/Makefile index ae7858b9772d..69d11aa30034 100644 --- a/tests/sys/cddl/zfs/tests/write_dirs/Makefile +++ b/tests/sys/cddl/zfs/tests/write_dirs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/xattr/Makefile b/tests/sys/cddl/zfs/tests/xattr/Makefile index 7a88513edc1c..a1f76ef79d72 100644 --- a/tests/sys/cddl/zfs/tests/xattr/Makefile +++ b/tests/sys/cddl/zfs/tests/xattr/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zfsd/Makefile b/tests/sys/cddl/zfs/tests/zfsd/Makefile index 7d3f29a7359e..e34e24b40906 100644 --- a/tests/sys/cddl/zfs/tests/zfsd/Makefile +++ b/tests/sys/cddl/zfs/tests/zfsd/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib b/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib index a4a7ef80ed9f..6369387ba069 100644 --- a/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib @@ -52,7 +52,7 @@ function wait_for_pool_dev_state_change function wait_for_pool_removal { typeset -i timeout=$1 - wait_for_pool_dev_state_change $timeout $REMOVAL_DISK "REMOVED|UNAVAIL" + wait_for_pool_dev_state_change $timeout $REMOVAL_DISK "REMOVED|UNAVAIL|FAULTED" } function wait_until_scrubbed diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh index 4eb04d60809e..7ad7a9113402 100644 --- a/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh @@ -83,19 +83,25 @@ typeset SPARE_NOP=${DISK4}.nop typeset OTHER_DISKS="${DISK1} ${DISK2}" typeset OTHER_NOPS=${OTHER_DISKS//~(E)([[:space:]]+|$)/.nop\1} set -A MY_KEYWORDS "mirror" "raidz1" "raidz2" +set -A MY_FAILURES "FAULTED" "REMOVED" ensure_zfsd_running log_must create_gnops $OTHER_DISKS $SPARE_DISK -for keyword in "${MY_KEYWORDS[@]}" ; do - log_must create_gnop $REMOVAL_DISK $PHYSPATH - log_must create_pool $TESTPOOL $keyword $REMOVAL_NOP $OTHER_NOPS spare $SPARE_NOP - log_must $ZPOOL set autoreplace=on $TESTPOOL +for failure in "${MY_FAILURES[@]}" ; do + for keyword in "${MY_KEYWORDS[@]}" ; do + log_must create_gnop $REMOVAL_DISK $PHYSPATH + log_must create_pool $TESTPOOL $keyword $REMOVAL_NOP $OTHER_NOPS spare $SPARE_NOP + log_must $ZPOOL set autoreplace=on $TESTPOOL - log_must destroy_gnop $REMOVAL_DISK - log_must wait_for_pool_removal 20 - log_must create_gnop $NEW_DISK $PHYSPATH - verify_assertion - destroy_pool "$TESTPOOL" - log_must destroy_gnop $NEW_DISK + if [ $failure = "FAULTED" ]; then + log_must zinject -d $REMOVAL_NOP -A fault $TESTPOOL + fi + log_must destroy_gnop $REMOVAL_DISK + log_must wait_for_pool_removal 20 + log_must create_gnop $NEW_DISK $PHYSPATH + verify_assertion + destroy_pool "$TESTPOOL" + log_must destroy_gnop $NEW_DISK + done done log_pass diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh index 3456a328e7f9..df704e183fb0 100644 --- a/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh @@ -78,6 +78,10 @@ for type in "raidz" "mirror"; do $DD if=/dev/zero bs=128k count=1 >> \ /$TESTPOOL/$TESTFS/$TESTFILE 2> /dev/null $FSYNC /$TESTPOOL/$TESTFS/$TESTFILE + # Due to a bug outside of zfsd, it may be necessary to reopen + # the pool before it will become DEGRADED. + # https://github.com/openzfs/zfs/issues/16245 + $ZPOOL reopen $TESTPOOL # Check to see if the pool is faulted yet $ZPOOL status $TESTPOOL | grep -q 'state: DEGRADED' if [ $? == 0 ] diff --git a/tests/sys/cddl/zfs/tests/zil/Makefile b/tests/sys/cddl/zfs/tests/zil/Makefile index 387e799de35e..db30bfc9f6ec 100644 --- a/tests/sys/cddl/zfs/tests/zil/Makefile +++ b/tests/sys/cddl/zfs/tests/zil/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zinject/Makefile b/tests/sys/cddl/zfs/tests/zinject/Makefile index a317d61aa37a..6f37d7060abf 100644 --- a/tests/sys/cddl/zfs/tests/zinject/Makefile +++ b/tests/sys/cddl/zfs/tests/zinject/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zones/Makefile b/tests/sys/cddl/zfs/tests/zones/Makefile index d06e7a613aea..4b27462d1c0b 100644 --- a/tests/sys/cddl/zfs/tests/zones/Makefile +++ b/tests/sys/cddl/zfs/tests/zones/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol/Makefile b/tests/sys/cddl/zfs/tests/zvol/Makefile index deeb1b027b33..a0c4899ae106 100644 --- a/tests/sys/cddl/zfs/tests/zvol/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile index 3f35f6b3ee8d..ffcc0f79e88c 100644 --- a/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile index d6cb44734f17..4282538eecbc 100644 --- a/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile index 19d944278c66..5f29f33a9b35 100644 --- a/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile index 50d1f728638f..21acc05d8446 100644 --- a/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile b/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile index bd3847db678e..71c8f330b156 100644 --- a/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile +++ b/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE=tests diff --git a/tests/sys/common/Makefile b/tests/sys/common/Makefile index 383fd17a5ba0..269aa9d52f9a 100644 --- a/tests/sys/common/Makefile +++ b/tests/sys/common/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/common ${PACKAGE}FILES+= vnet.subr diff --git a/tests/sys/common/vnet.subr b/tests/sys/common/vnet.subr index 183f7284d614..bd98b02da33f 100644 --- a/tests/sys/common/vnet.subr +++ b/tests/sys/common/vnet.subr @@ -11,11 +11,32 @@ unlist_interface() sed -i "" /^$1\$/d created_interfaces.lst } +_vnet_check_req() +{ + type=$1 + + if kldstat -q -n if_${type}.ko; then + return + fi + + if ! kldload -n -q if_${type}; then + atf_skip "if_${type}.ko is required to run this test." + return + fi +} + vnet_init() { if [ "`sysctl -i -n kern.features.vimage`" != 1 ]; then atf_skip "This test requires VIMAGE" fi + + # Check if we can create if_epair or if_bridge interfaces. + # We may be running in a jail already, unable to load modules. + # If so, skip this test because it very likely (but not certainly) + # wants at least one of those + _vnet_check_req epair + _vnet_check_req bridge } vnet_mkepair() @@ -26,6 +47,13 @@ vnet_mkepair() echo ${ifname%a} } +vnet_init_bridge() +{ + if ! kldstat -q -m if_bridge; then + atf_skip "This test requires if_bridge" + fi +} + vnet_mkbridge() { ifname=$(ifconfig bridge create) diff --git a/tests/sys/compat32/Makefile b/tests/sys/compat32/Makefile index 31834de16246..e1a35422410e 100644 --- a/tests/sys/compat32/Makefile +++ b/tests/sys/compat32/Makefile @@ -1,4 +1,3 @@ - .if exists(${.CURDIR}/${MACHINE_ARCH}) SUBDIR+= ${MACHINE_ARCH} .endif diff --git a/tests/sys/compat32/Makefile.inc b/tests/sys/compat32/Makefile.inc index 0220ac0431e9..7f870e3701fb 100644 --- a/tests/sys/compat32/Makefile.inc +++ b/tests/sys/compat32/Makefile.inc @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/compat32 .include "../Makefile.inc" diff --git a/tests/sys/devrandom/Makefile b/tests/sys/devrandom/Makefile index e8a28789a7a3..342925246aee 100644 --- a/tests/sys/devrandom/Makefile +++ b/tests/sys/devrandom/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> SDEVRANDOM= ${SRCTOP}/sys/dev/random diff --git a/tests/sys/fifo/Makefile b/tests/sys/fifo/Makefile index 766e9ee452a6..6d4c859288d9 100644 --- a/tests/sys/fifo/Makefile +++ b/tests/sys/fifo/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/fifo PLAIN_TESTS_C+= fifo_create diff --git a/tests/sys/file/Makefile b/tests/sys/file/Makefile index 010606bfeba0..f80d1b271b85 100644 --- a/tests/sys/file/Makefile +++ b/tests/sys/file/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/file BINDIR= ${TESTSDIR} diff --git a/tests/sys/file/path_test.c b/tests/sys/file/path_test.c index 911c7c7075f0..b3b8b7cebd4d 100644 --- a/tests/sys/file/path_test.c +++ b/tests/sys/file/path_test.c @@ -684,10 +684,14 @@ ATF_TC_BODY(path_io, tc) size_t page_size; int error, fd, pathfd, sd[2]; - /* It shouldn't be possible to create new files with O_PATH. */ + /* It is allowed to create new files with O_PATH. */ snprintf(path, sizeof(path), "path_io.XXXXXX"); ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp")); - ATF_REQUIRE_ERRNO(ENOENT, open(path, O_PATH | O_CREAT, 0600) < 0); + pathfd = open(path, O_PATH | O_CREAT, 0600); + ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open(O_PATH|O_CREAT)")); + /* Ensure that this is indeed O_PATH fd */ + ATF_REQUIRE_ERRNO(EBADF, write(pathfd, path, strlen(path)) == -1); + CHECKED_CLOSE(pathfd); /* Create a non-empty file for use in the rest of the tests. */ mktfile(path, "path_io.XXXXXX"); diff --git a/tests/sys/fs/Makefile b/tests/sys/fs/Makefile index f0fd2cc5955f..e36a97d2335a 100644 --- a/tests/sys/fs/Makefile +++ b/tests/sys/fs/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> .include <bsd.compiler.mk> diff --git a/tests/sys/fs/Makefile.inc b/tests/sys/fs/Makefile.inc index d3b5cbd3a79b..01b5f23410c8 100644 --- a/tests/sys/fs/Makefile.inc +++ b/tests/sys/fs/Makefile.inc @@ -1,2 +1 @@ - .include "../Makefile.inc" diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index f45f2f93e1c0..b11f11bdfa98 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -1,10 +1,11 @@ - .include <bsd.compiler.mk> PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/fs/fusefs +ATF_TESTS_SH+= ctl + # We could simply link all of these files into a single executable. But since # Kyua treats googletest programs as plain tests, it's better to separate them # out, so we get more granular reporting. @@ -40,6 +41,7 @@ GTESTS+= nfs GTESTS+= notify GTESTS+= open GTESTS+= opendir +GTESTS+= pre-init GTESTS+= read GTESTS+= readdir GTESTS+= readlink @@ -56,7 +58,6 @@ GTESTS+= xattr .for p in ${GTESTS} SRCS.$p+= ${p}.cc -SRCS.$p+= getmntopts.c SRCS.$p+= mockfs.cc SRCS.$p+= utils.cc .endfor @@ -65,12 +66,13 @@ TEST_METADATA.default_permissions+= required_user="unprivileged" TEST_METADATA.default_permissions_privileged+= required_user="root" TEST_METADATA.mknod+= required_user="root" TEST_METADATA.nfs+= required_user="root" +# ctl must be exclusive because it disables/enables camsim +TEST_METADATA.ctl+= is_exclusive="true" +TEST_METADATA.ctl+= required_user="root" -# TODO: drastically increase timeout after test development is mostly complete TEST_METADATA+= timeout=10 FUSEFS= ${SRCTOP}/sys/fs/fuse -MOUNT= ${SRCTOP}/sbin/mount # Suppress warnings that GCC generates for the libc++ and gtest headers. CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes # Suppress Wcast-align for readdir.cc, because it is unavoidable when using @@ -89,9 +91,6 @@ CXXWARNFLAGS+= -Wno-vla-cxx-extension .endif CXXFLAGS+= -I${SRCTOP}/tests CXXFLAGS+= -I${FUSEFS} -CXXFLAGS+= -I${MOUNT} -.PATH: ${MOUNT} -CXXSTD= c++14 LIBADD+= pthread LIBADD+= gmock gtest diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc index dae6290ea8e5..24a161166a90 100644 --- a/tests/sys/fs/fusefs/allow_other.cc +++ b/tests/sys/fs/fusefs/allow_other.cc @@ -52,9 +52,6 @@ const static char RELPATH[] = "some_file.txt"; class NoAllowOther: public FuseTest { public: -/* Unprivileged user id */ -int m_uid; - virtual void SetUp() { if (geteuid() != 0) { GTEST_SKIP() << "This test must be run as root"; diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc index 4c9edac9360a..30612079657d 100644 --- a/tests/sys/fs/fusefs/bmap.cc +++ b/tests/sys/fs/fusefs/bmap.cc @@ -77,8 +77,6 @@ class BmapEof: public Bmap, public WithParamInterface<int> {}; /* * Test FUSE_BMAP - * XXX The FUSE protocol does not include the runp and runb variables, so those - * must be guessed in-kernel. */ TEST_F(Bmap, bmap) { @@ -105,8 +103,19 @@ TEST_F(Bmap, bmap) arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, pbn); - EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); - EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); + /* + * XXX The FUSE protocol does not include the runp and runb variables, + * so those must be guessed in-kernel. There's no "right" answer, so + * just check that they're within reasonable limits. + */ + EXPECT_LE(arg.runb, lbn); + EXPECT_LE((unsigned long)arg.runb, m_maxreadahead / m_maxbcachebuf); + EXPECT_LE((unsigned long)arg.runb, m_maxphys / m_maxbcachebuf); + EXPECT_GT(arg.runb, 0); + EXPECT_LE(arg.runp, filesize / m_maxbcachebuf - lbn); + EXPECT_LE((unsigned long)arg.runp, m_maxreadahead / m_maxbcachebuf); + EXPECT_LE((unsigned long)arg.runp, m_maxphys / m_maxbcachebuf); + EXPECT_GT(arg.runp, 0); leak(fd); } @@ -142,7 +151,7 @@ TEST_F(Bmap, default_) arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, 0); - EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); EXPECT_EQ(arg.runb, 0); /* In the middle */ @@ -152,8 +161,8 @@ TEST_F(Bmap, default_) arg.runb = -1; ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); - EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1); - EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); /* Last block */ lbn = filesize / m_maxbcachebuf - 1; @@ -163,7 +172,7 @@ TEST_F(Bmap, default_) ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno); EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE); EXPECT_EQ(arg.runp, 0); - EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1); + EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1); leak(fd); } diff --git a/tests/sys/fs/fusefs/ctl.sh b/tests/sys/fs/fusefs/ctl.sh new file mode 100644 index 000000000000..7d2e7593cbdc --- /dev/null +++ b/tests/sys/fs/fusefs/ctl.sh @@ -0,0 +1,69 @@ +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 ConnectWise +# All rights reserved. +# +# 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 DOCUMENTATION IS PROVIDED BY THE AUTHOR ``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 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. + +. $(atf_get_srcdir)/../../cam/ctl/ctl.subr + +# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283402 +# +# Almost any fuse file system would work, but this tests uses fusefs-ext2 +# because it's simple and its download is very small. +atf_test_case remove_lun_with_atime cleanup +remove_lun_with_atime_head() +{ + atf_set "descr" "Remove a fuse-backed CTL LUN when atime is enabled" + atf_set "require.user" "root" + atf_set "require.progs" "fuse-ext2 mkfs.ext2" +} +remove_lun_with_atime_body() +{ + MOUNTPOINT=$PWD/mnt + atf_check mkdir $MOUNTPOINT + atf_check truncate -s 1g ext2.img + atf_check mkfs.ext2 -q ext2.img + # Note: both default_permissions and atime must be enabled + atf_check fuse-ext2 -o default_permissions,allow_other,rw+ ext2.img \ + $MOUNTPOINT + + atf_check truncate -s 1m $MOUNTPOINT/file + create_block -o file=$MOUNTPOINT/file + + # Force fusefs to open the file, and dirty its atime + atf_check dd if=/dev/$dev of=/dev/null count=1 status=none + + # Finally, remove the LUN. Hopefully it won't panic. + atf_check -o ignore ctladm remove -b block -l $LUN + + rm lun-create.txt # So we don't try to remove the LUN twice +} +remove_lun_with_atime_cleanup() +{ + cleanup + umount $PWD/mnt +} + +atf_init_test_cases() +{ + atf_add_test_case remove_lun_with_atime +} diff --git a/tests/sys/fs/fusefs/destroy.cc b/tests/sys/fs/fusefs/destroy.cc index 16d50da19b9b..45acb1f99724 100644 --- a/tests/sys/fs/fusefs/destroy.cc +++ b/tests/sys/fs/fusefs/destroy.cc @@ -60,7 +60,7 @@ static void* open_th(void* arg) { * Check for any memory leaks like this: * 1) kldunload fusefs, if necessary * 2) kldload fusefs - * 3) ./destroy --gtest_filter=Destroy.unsent_operations + * 3) ./destroy --gtest_filter=Death.unsent_operations * 4) kldunload fusefs * 5) check /var/log/messages for anything like this: Freed UMA keg (fuse_ticket) was not empty (31 items). Lost 2 pages of memory. diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc index ff5e3eb4f4bb..4e5b047b78b7 100644 --- a/tests/sys/fs/fusefs/fallocate.cc +++ b/tests/sys/fs/fusefs/fallocate.cc @@ -32,10 +32,9 @@ extern "C" { #include <sys/time.h> #include <fcntl.h> +#include <mntopts.h> // for build_iovec #include <signal.h> #include <unistd.h> - -#include "mntopts.h" // for build_iovec } #include "mockfs.hh" @@ -310,6 +309,57 @@ TEST_F(Fspacectl, erofs) leak(fd); } +/* + * If FUSE_GETATTR fails when determining the size of the file, fspacectl + * should fail gracefully. This failure mode is easiest to trigger when + * attribute caching is disabled. + */ +TEST_F(Fspacectl, getattr_fails) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + Sequence seq; + struct spacectl_range rqsr; + const uint64_t ino = 42; + const uint64_t fsize = 2000; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(1) + .InSequence(seq) + .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = ino; + out.body.attr.attr.mode = S_IFREG | 0644; + out.body.attr.attr.size = fsize; + out.body.attr.attr_valid = 0; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).InSequence(seq) + .WillOnce(ReturnErrno(EIO)); + + fd = open(FULLPATH, O_RDWR); + ASSERT_LE(0, fd) << strerror(errno); + rqsr.r_offset = 500; + rqsr.r_len = 1000; + EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL)); + EXPECT_EQ(EIO, errno); + + leak(fd); +} + TEST_F(Fspacectl, ok) { const char FULLPATH[] = "mountpoint/some_file.txt"; diff --git a/tests/sys/fs/fusefs/flush.cc b/tests/sys/fs/fusefs/flush.cc index 474cdbdb2203..7ba1218b3287 100644 --- a/tests/sys/fs/fusefs/flush.cc +++ b/tests/sys/fs/fusefs/flush.cc @@ -109,6 +109,36 @@ TEST_F(Flush, open_twice) EXPECT_EQ(0, close(fd)) << strerror(errno); } +/** + * Test for FOPEN_NOFLUSH: we expect that zero flush calls will be performed. + */ +TEST_F(Flush, open_noflush) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + uint64_t pid = (uint64_t)getpid(); + int fd; + + expect_lookup(RELPATH, ino, 1); + expect_open(ino, FOPEN_NOFLUSH, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_FLUSH && + in.header.nodeid == ino && + in.body.flush.lock_owner == pid && + in.body.flush.fh == FH); + }, Eq(true)), + _) + ).Times(0); + expect_release(); + + fd = open(FULLPATH, O_WRONLY); + ASSERT_LE(0, fd) << strerror(errno); + // close MUST not flush + EXPECT_EQ(0, close(fd)) << strerror(errno); +} + /* * Some FUSE filesystem cache data internally and flush it on release. Such * filesystems may generate errors during release. On Linux, these get diff --git a/tests/sys/fs/fusefs/forget.cc b/tests/sys/fs/fusefs/forget.cc index 846198e75925..1e7764ac4782 100644 --- a/tests/sys/fs/fusefs/forget.cc +++ b/tests/sys/fs/fusefs/forget.cc @@ -31,7 +31,6 @@ extern "C" { #include <sys/types.h> #include <sys/mount.h> -#include <sys/sysctl.h> #include <fcntl.h> #include <semaphore.h> diff --git a/tests/sys/fs/fusefs/io.cc b/tests/sys/fs/fusefs/io.cc index 99b5eae34e09..ced291836da0 100644 --- a/tests/sys/fs/fusefs/io.cc +++ b/tests/sys/fs/fusefs/io.cc @@ -31,13 +31,14 @@ extern "C" { #include <sys/types.h> #include <sys/mman.h> -#include <sys/sysctl.h> #include <fcntl.h> #include <stdlib.h> #include <unistd.h> } +#include <iomanip> + #include "mockfs.hh" #include "utils.hh" diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc index 495bfd8aa959..5fcd3c36c892 100644 --- a/tests/sys/fs/fusefs/last_local_modify.cc +++ b/tests/sys/fs/fusefs/last_local_modify.cc @@ -233,7 +233,6 @@ TEST_P(LastLocalModify, lookup) SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; - out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; @@ -277,6 +276,7 @@ TEST_P(LastLocalModify, lookup) SET_OUT_HEADER_LEN(*out0, entry); out0->body.entry.attr.mode = S_IFREG | 0644; out0->body.entry.nodeid = ino; + out0->body.entry.attr.ino = ino; out0->body.entry.entry_valid = UINT64_MAX; out0->body.entry.attr_valid = UINT64_MAX; out0->body.entry.attr.size = oldsize; @@ -392,7 +392,6 @@ TEST_P(LastLocalModify, vfs_vget) SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; - out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; @@ -414,7 +413,6 @@ TEST_P(LastLocalModify, vfs_vget) SET_OUT_HEADER_LEN(out, entry); out.body.entry.nodeid = ino; out.body.entry.attr.size = oldsize; - out.body.entry.nodeid = ino; out.body.entry.attr_valid_nsec = NAP_NS / 2; out.body.entry.attr.ino = ino; out.body.entry.attr.mode = S_IFREG | 0644; @@ -439,6 +437,7 @@ TEST_P(LastLocalModify, vfs_vget) SET_OUT_HEADER_LEN(*out0, entry); out0->body.entry.attr.mode = S_IFREG | 0644; out0->body.entry.nodeid = ino; + out0->body.entry.attr.ino = ino; out0->body.entry.entry_valid = UINT64_MAX; out0->body.entry.attr_valid = UINT64_MAX; out0->body.entry.attr.size = oldsize; diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc index 6d506c1ab700..2cfe888b6b08 100644 --- a/tests/sys/fs/fusefs/lookup.cc +++ b/tests/sys/fs/fusefs/lookup.cc @@ -560,6 +560,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; // immediate timeout }))); @@ -568,6 +569,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = bar_ino; + out.body.entry.attr.ino = bar_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -577,6 +579,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr.ino = FUSE_ROOT_ID; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -607,6 +610,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -615,6 +619,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = bar_ino; + out.body.entry.attr.ino = bar_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -632,6 +637,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = foo_ino; + out.body.entry.attr.ino = foo_ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -640,6 +646,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = FUSE_ROOT_ID; + out.body.entry.attr.ino = FUSE_ROOT_ID; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc index 5ffeb4b33cbd..12d41f7af1b2 100644 --- a/tests/sys/fs/fusefs/lseek.cc +++ b/tests/sys/fs/fusefs/lseek.cc @@ -71,6 +71,7 @@ TEST_F(LseekPathconf, already_enosys) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA)); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); @@ -105,6 +106,7 @@ TEST_F(LseekPathconf, already_seeked) out.body.lseek.offset = i.body.lseek.offset; }))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA)); EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); @@ -113,6 +115,76 @@ TEST_F(LseekPathconf, already_seeked) } /* + * Use pathconf on a file not already opened. The server returns EACCES when + * the kernel tries to open it. The kernel should return EACCES, and make no + * judgement about whether the server does or does not support FUSE_LSEEK. + */ +TEST_F(LseekPathconf, eacces) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.entry_valid = UINT64_MAX; + out.body.entry.attr.mode = S_IFREG | 0644; + out.body.entry.nodeid = ino; + out.body.entry.attr.size = fsize; + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_OPEN && + in.header.nodeid == ino); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnErrno(EACCES))); + + EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EACCES, errno); + /* Check again, to ensure that the kernel didn't record the response */ + EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EACCES, errno); +} + +/* + * If the server returns some weird error when we try FUSE_LSEEK, send that to + * the caller but don't record the answer. + */ +TEST_F(LseekPathconf, eio) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).Times(2) + .WillRepeatedly(Invoke(ReturnErrno(EIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EIO, errno); + /* Check again, to ensure that the kernel didn't record the response */ + EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + EXPECT_EQ(EIO, errno); + + leak(fd); +} + +/* * If no FUSE_LSEEK operation has been attempted since mount, try once as soon * as a pathconf request comes in. */ @@ -134,6 +206,7 @@ TEST_F(LseekPathconf, enosys_now) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); EXPECT_EQ(EINVAL, errno); @@ -142,6 +215,34 @@ TEST_F(LseekPathconf, enosys_now) } /* + * Use pathconf, rather than fpathconf, on a file not already opened. + * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135 + */ +TEST_F(LseekPathconf, pathconf) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 1 << 30; /* 1 GiB */ + off_t offset_out = 1 << 29; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) { + SET_OUT_HEADER_LEN(out, lseek); + out.body.lseek.offset = offset_out; + }))); + expect_release(ino, FuseTest::FH); + + EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno); +} + +/* * If no FUSE_LSEEK operation has been attempted since mount, try one as soon * as a pathconf request comes in. This is the typical pattern of bsdtar. It * will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported. @@ -169,6 +270,7 @@ TEST_F(LseekPathconf, seek_now) }))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET)); EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); /* And check that the file pointer hasn't changed */ @@ -178,6 +280,39 @@ TEST_F(LseekPathconf, seek_now) } /* + * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or + * zero-length file, then SEEK_DATA will return ENXIO. That should be + * interpreted as success. + */ +TEST_F(LseekPathconf, zerolength) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + const uint64_t ino = 42; + off_t fsize = 0; + int fd; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1); + expect_open(ino, 0, 1); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_LSEEK && + in.header.nodeid == ino && + in.body.lseek.whence == SEEK_DATA); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnErrno(ENXIO))); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + /* Check again, to ensure that the kernel recorded the response */ + EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); + + leak(fd); +} + +/* * For servers using older protocol versions, no FUSE_LSEEK should be attempted */ TEST_F(LseekPathconf_7_23, already_enosys) @@ -198,6 +333,7 @@ TEST_F(LseekPathconf_7_23, already_enosys) ).Times(0); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE)); EXPECT_EQ(EINVAL, errno); @@ -262,6 +398,7 @@ TEST_F(LseekSeekData, enosys) _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); /* * Default behavior: ENXIO if offset is < 0 or >= fsize, offset @@ -302,6 +439,7 @@ TEST_F(LseekSeekHole, ok) out.body.lseek.offset = offset_out; }))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE)); EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR)); @@ -334,6 +472,7 @@ TEST_F(LseekSeekHole, enosys) _) ).WillOnce(Invoke(ReturnErrno(ENOSYS))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); /* * Default behavior: ENXIO if offset is < 0 or >= fsize, fsize @@ -371,6 +510,7 @@ TEST_F(LseekSeekHole, enxio) _) ).WillOnce(Invoke(ReturnErrno(ENXIO))); fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd); EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE)); EXPECT_EQ(ENXIO, errno); diff --git a/tests/sys/fs/fusefs/mknod.cc b/tests/sys/fs/fusefs/mknod.cc index 1fb855f44f29..eb745f19acd5 100644 --- a/tests/sys/fs/fusefs/mknod.cc +++ b/tests/sys/fs/fusefs/mknod.cc @@ -283,7 +283,7 @@ TEST_F(Mknod, parent_inode) } /* - * fusefs(5) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a + * fusefs(4) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a * feature, not a bug */ TEST_F(Mknod, DISABLED_whiteout) diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index bd7bd1b663f9..65cdc3919652 100644 --- a/tests/sys/fs/fusefs/mockfs.cc +++ b/tests/sys/fs/fusefs/mockfs.cc @@ -39,13 +39,12 @@ extern "C" { #include <fcntl.h> #include <libutil.h> +#include <mntopts.h> // for build_iovec #include <poll.h> #include <pthread.h> #include <signal.h> #include <stdlib.h> #include <unistd.h> - -#include "mntopts.h" // for build_iovec } #include <cinttypes> @@ -416,11 +415,25 @@ void MockFS::debug_response(const mockfs_buf_out &out) { } } -MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, +MockFS::MockFS(int max_read, int max_readahead, bool allow_other, + bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version, uint32_t max_write, bool async, bool noclusterr, unsigned time_gran, bool nointr, bool noatime, - const char *fsname, const char *subtype) + const char *fsname, const char *subtype, bool no_auto_init) + : m_daemon_id(NULL), + m_kernel_minor_version(kernel_minor_version), + m_kq(pm == KQ ? kqueue() : -1), + m_maxread(max_read), + m_maxreadahead(max_readahead), + m_pid(getpid()), + m_uniques(new std::unordered_set<uint64_t>), + m_pm(pm), + m_time_gran(time_gran), + m_child_pid(-1), + m_maxwrite(MIN(max_write, max_max_write)), + m_nready(-1), + m_quit(false) { struct sigaction sa; struct iovec *iov = NULL; @@ -428,20 +441,6 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, char fdstr[15]; const bool trueval = true; - m_daemon_id = NULL; - m_kernel_minor_version = kernel_minor_version; - m_maxreadahead = max_readahead; - m_maxwrite = MIN(max_write, max_max_write); - m_nready = -1; - m_pm = pm; - m_time_gran = time_gran; - m_quit = false; - m_last_unique = 0; - if (m_pm == KQ) - m_kq = kqueue(); - else - m_kq = -1; - /* * Kyua sets pwd to a testcase-unique tempdir; no need to use * mkdtemp @@ -466,15 +465,18 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, throw(std::system_error(errno, std::system_category(), "Couldn't open /dev/fuse")); - m_pid = getpid(); - m_child_pid = -1; - build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1); build_iovec(&iov, &iovlen, "fspath", __DECONST(void *, "mountpoint"), -1); build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1); sprintf(fdstr, "%d", m_fuse_fd); build_iovec(&iov, &iovlen, "fd", fdstr, -1); + if (m_maxread > 0) { + char val[10]; + + snprintf(val, sizeof(val), "%d", m_maxread); + build_iovec(&iov, &iovlen, "max_read=", &val, -1); + } if (allow_other) { build_iovec(&iov, &iovlen, "allow_other", __DECONST(void*, &trueval), sizeof(bool)); @@ -527,7 +529,9 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, ON_CALL(*this, process(_, _)) .WillByDefault(Invoke(this, &MockFS::process_default)); - init(flags); + if (!no_auto_init) + init(flags); + bzero(&sa, sizeof(sa)); sa.sa_handler = sigint_handler; sa.sa_flags = 0; /* Don't set SA_RESTART! */ @@ -541,10 +545,7 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions, MockFS::~MockFS() { kill_daemon(); - if (m_daemon_id != NULL) { - pthread_join(m_daemon_id, NULL); - m_daemon_id = NULL; - } + join_daemon(); ::unmount("mountpoint", MNT_FORCE); rmdir("mountpoint"); if (m_kq >= 0) @@ -738,14 +739,10 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) { default: FAIL() << "Unknown opcode " << in.header.opcode; } - /* - * Check that the ticket's unique value is sequential. Technically it - * doesn't need to be sequential, merely unique. But the current - * fusefs driver _does_ make it sequential, and that's easy to check - * for. - */ - if (in.header.unique != ++m_last_unique) - FAIL() << "Non-sequential unique value"; + /* Verify that the ticket's unique value is actually unique. */ + if (m_uniques->find(in.header.unique) != m_uniques->end()) + FAIL() << "Non-unique \"unique\" value"; + m_uniques->insert(in.header.unique); } void MockFS::init(uint32_t flags) { @@ -789,6 +786,13 @@ void MockFS::kill_daemon() { m_fuse_fd = -1; } +void MockFS::join_daemon() { + if (m_daemon_id != NULL) { + pthread_join(m_daemon_id, NULL); + m_daemon_id = NULL; + } +} + void MockFS::loop() { std::vector<std::unique_ptr<mockfs_buf_out>> out; @@ -1035,6 +1039,10 @@ void MockFS::write_response(const mockfs_buf_out &out) { ASSERT_EQ(-1, r); ASSERT_EQ(out.expected_errno, errno) << strerror(errno); } else { + if (r <= 0 && errno == EINVAL) { + printf("Failed to write response. unique=%" PRIu64 + ":\n", out.header.unique); + } ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno); } } diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index 958964f769d4..ba6f7fded9d0 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -36,6 +36,8 @@ extern "C" { #include "fuse_kernel.h" } +#include <unordered_set> + #include <gmock/gmock.h> #define TIME_T_MAX (std::numeric_limits<time_t>::max()) @@ -292,14 +294,20 @@ class MockFS { int m_kq; + /* + * If nonzero, the maximum size in bytes of a read that the kernel will + * send to the server. + */ + int m_maxread; + /* The max_readahead file system option */ uint32_t m_maxreadahead; /* pid of the test process */ pid_t m_pid; - /* The unique value of the header of the last received operation */ - uint64_t m_last_unique; + /* Every "unique" value of a fuse ticket seen so far */ + std::unique_ptr<std::unordered_set<uint64_t>> m_uniques; /* Method the daemon should use for I/O to and from /dev/fuse */ enum poll_method m_pm; @@ -353,18 +361,22 @@ class MockFS { bool m_quit; /* Create a new mockfs and mount it to a tempdir */ - MockFS(int max_readahead, bool allow_other, + MockFS(int max_read, int max_readahead, bool allow_other, bool default_permissions, bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags, uint32_t kernel_minor_version, uint32_t max_write, bool async, bool no_clusterr, unsigned time_gran, bool nointr, - bool noatime, const char *fsname, const char *subtype); + bool noatime, const char *fsname, const char *subtype, + bool no_auto_init); virtual ~MockFS(); /* Kill the filesystem daemon without unmounting the filesystem */ void kill_daemon(); + /* Wait until the daemon thread terminates */ + void join_daemon(); + /* Process FUSE requests endlessly */ void loop(); diff --git a/tests/sys/fs/fusefs/mount.cc b/tests/sys/fs/fusefs/mount.cc index 7a8d2c1396f0..ece518b09f66 100644 --- a/tests/sys/fs/fusefs/mount.cc +++ b/tests/sys/fs/fusefs/mount.cc @@ -33,7 +33,7 @@ extern "C" { #include <sys/mount.h> #include <sys/uio.h> -#include "mntopts.h" // for build_iovec +#include <mntopts.h> // for build_iovec } #include "mockfs.hh" diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc index 79fead8e77cb..2fa2b290f383 100644 --- a/tests/sys/fs/fusefs/nfs.cc +++ b/tests/sys/fs/fusefs/nfs.cc @@ -84,6 +84,7 @@ TEST_F(Fhstat, estale) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; @@ -95,6 +96,7 @@ TEST_F(Fhstat, estale) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 2; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; @@ -121,6 +123,7 @@ TEST_F(Fhstat, lookup_dot) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr.uid = uid; out.body.entry.attr_valid = UINT64_MAX; @@ -132,6 +135,7 @@ TEST_F(Fhstat, lookup_dot) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr.uid = uid; out.body.entry.attr_valid = UINT64_MAX; @@ -144,6 +148,37 @@ TEST_F(Fhstat, lookup_dot) EXPECT_EQ(mode, sb.st_mode); } +/* Gracefully handle failures to lookup ".". */ +TEST_F(Fhstat, lookup_dot_error) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino = 42; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.attr.mode = mode; + out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino, ".") + .WillOnce(Invoke(ReturnErrno(EDOOFUS))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + ASSERT_EQ(-1, fhstat(&fhp, &sb)); + EXPECT_EQ(EDOOFUS, errno); +} + /* Use a file handle whose entry is still cached */ TEST_F(Fhstat, cached) { @@ -159,6 +194,7 @@ TEST_F(Fhstat, cached) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr.ino = ino; out.body.entry.attr_valid = UINT64_MAX; @@ -185,6 +221,7 @@ TEST_F(Fhstat, cache_expired) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr.ino = ino; out.body.entry.attr_valid = UINT64_MAX; @@ -196,6 +233,7 @@ TEST_F(Fhstat, cache_expired) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr.ino = ino; out.body.entry.attr_valid = UINT64_MAX; @@ -213,6 +251,99 @@ TEST_F(Fhstat, cache_expired) EXPECT_EQ(ino, sb.st_ino); } +/* + * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't + * lookup, it's a bug. But we should handle it gracefully. + */ +TEST_F(Fhstat, inconsistent_nodeid) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t ino_in = 42; + const uint64_t ino_out = 43; + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino_in; + out.body.entry.attr.ino = ino_in; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(ino_in, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = ino_out; + out.body.entry.attr.ino = ino_out; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(EIO, errno); +} + +/* + * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match + * the inode number, and the file system is exported, it's a bug. But we + * should handle it gracefully. + */ +TEST_F(Fhstat, inconsistent_ino) +{ + const char FULLPATH[] = "mountpoint/some_dir/."; + const char RELDIRPATH[] = "some_dir"; + fhandle_t fhp; + struct stat sb; + const uint64_t nodeid = 42; + const uint64_t ino = 711; // Could be anything that != nodeid + const mode_t mode = S_IFDIR | 0755; + const uid_t uid = 12345; + + EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH) + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = nodeid; + out.body.entry.attr.ino = nodeid; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + EXPECT_LOOKUP(nodeid, ".") + .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) { + SET_OUT_HEADER_LEN(out, entry); + out.body.entry.nodeid = nodeid; + out.body.entry.attr.ino = ino; + out.body.entry.attr.mode = mode; + out.body.entry.generation = 1; + out.body.entry.attr.uid = uid; + out.body.entry.attr_valid = UINT64_MAX; + out.body.entry.entry_valid = 0; + }))); + + ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno); + /* + * The fhstat operation will actually succeed. But future operations + * will likely fail. + */ + ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno); + EXPECT_EQ(ino, sb.st_ino); +} + /* * If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style * lookups @@ -230,6 +361,7 @@ TEST_F(FhstatNotExportable, lookup_dot) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; @@ -252,6 +384,7 @@ TEST_F(Getfh, eoverflow) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = (uint64_t)UINT32_MAX + 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; @@ -274,6 +407,7 @@ TEST_F(Getfh, ok) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = S_IFDIR | 0755; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = UINT64_MAX; }))); @@ -305,6 +439,7 @@ TEST_F(Readdir, getdirentries) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; @@ -315,6 +450,7 @@ TEST_F(Readdir, getdirentries) SET_OUT_HEADER_LEN(out, entry); out.body.entry.attr.mode = mode; out.body.entry.nodeid = ino; + out.body.entry.attr.ino = ino; out.body.entry.generation = 1; out.body.entry.attr_valid = UINT64_MAX; out.body.entry.entry_valid = 0; diff --git a/tests/sys/fs/fusefs/notify.cc b/tests/sys/fs/fusefs/notify.cc index e3f539f57599..1e22bde13db7 100644 --- a/tests/sys/fs/fusefs/notify.cc +++ b/tests/sys/fs/fusefs/notify.cc @@ -30,7 +30,6 @@ extern "C" { #include <sys/types.h> -#include <sys/sysctl.h> #include <fcntl.h> #include <pthread.h> diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc index 7ab3aeb6ba2a..1212a7047f26 100644 --- a/tests/sys/fs/fusefs/open.cc +++ b/tests/sys/fs/fusefs/open.cc @@ -70,16 +70,8 @@ void test_ok(int os_flags, int fuse_flags) { } }; - -class OpenNoOpenSupport: public FuseTest { - virtual void SetUp() { - m_init_flags = FUSE_NO_OPEN_SUPPORT; - FuseTest::SetUp(); - } -}; - /* - * fusefs(5) does not support I/O on device nodes (neither does UFS). But it + * fusefs(4) does not support I/O on device nodes (neither does UFS). But it * shouldn't crash */ TEST_F(Open, chr) @@ -281,37 +273,11 @@ TEST_F(Open, o_rdwr) } /* - * Without FUSE_NO_OPEN_SUPPORT, returning ENOSYS is an error - */ -TEST_F(Open, enosys) -{ - const char FULLPATH[] = "mountpoint/some_file.txt"; - const char RELPATH[] = "some_file.txt"; - uint64_t ino = 42; - int fd; - - FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); - EXPECT_CALL(*m_mock, process( - ResultOf([=](auto in) { - return (in.header.opcode == FUSE_OPEN && - in.body.open.flags == (uint32_t)O_RDONLY && - in.header.nodeid == ino); - }, Eq(true)), - _) - ).Times(1) - .WillOnce(Invoke(ReturnErrno(ENOSYS))); - - fd = open(FULLPATH, O_RDONLY); - ASSERT_EQ(-1, fd) << strerror(errno); - EXPECT_EQ(ENOSYS, errno); -} - -/* - * If a fuse server sets FUSE_NO_OPEN_SUPPORT and returns ENOSYS to a + * If a fuse server returns ENOSYS to a * FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will * also succeed automatically without being sent to the server. */ -TEST_F(OpenNoOpenSupport, enosys) +TEST_F(Open, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc index dd837a8d43c1..e1fed59635fc 100644 --- a/tests/sys/fs/fusefs/opendir.cc +++ b/tests/sys/fs/fusefs/opendir.cc @@ -71,13 +71,6 @@ void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r) }; -class OpendirNoOpendirSupport: public Opendir { - virtual void SetUp() { - m_init_flags = FUSE_NO_OPENDIR_SUPPORT; - FuseTest::SetUp(); - } -}; - /* * The fuse daemon fails the request with enoent. This usually indicates a @@ -179,27 +172,11 @@ TEST_F(Opendir, opendir) } /* - * Without FUSE_NO_OPENDIR_SUPPORT, returning ENOSYS is an error - */ -TEST_F(Opendir, enosys) -{ - const char FULLPATH[] = "mountpoint/some_file.txt"; - const char RELPATH[] = "some_file.txt"; - uint64_t ino = 42; - - expect_lookup(RELPATH, ino); - expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS)); - - EXPECT_EQ(-1, open(FULLPATH, O_DIRECTORY)); - EXPECT_EQ(ENOSYS, errno); -} - -/* - * If a fuse server sets FUSE_NO_OPENDIR_SUPPORT and returns ENOSYS to a + * If a fuse server returns ENOSYS to a * FUSE_OPENDIR, then it and subsequent FUSE_OPENDIR and FUSE_RELEASEDIR * operations will also succeed automatically without being sent to the server. */ -TEST_F(OpendirNoOpendirSupport, enosys) +TEST_F(Opendir, enosys) { const char FULLPATH[] = "mountpoint/some_file.txt"; const char RELPATH[] = "some_file.txt"; diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc new file mode 100644 index 000000000000..e990d3cafffa --- /dev/null +++ b/tests/sys/fs/fusefs/pre-init.cc @@ -0,0 +1,154 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 ConnectWise + * + * 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. + */ + +extern "C" { +#include <sys/param.h> +#include <sys/mount.h> +#include <sys/signal.h> +#include <sys/wait.h> + +#include <fcntl.h> +#include <pthread.h> +#include <semaphore.h> +#include <signal.h> +} + +#include "mockfs.hh" +#include "utils.hh" + +using namespace testing; + +/* Tests for behavior that happens before the server responds to FUSE_INIT */ +class PreInit: public FuseTest { +void SetUp() { + m_no_auto_init = true; + FuseTest::SetUp(); +} +}; + +static void* unmount1(void* arg __unused) { + ssize_t r; + + r = unmount("mountpoint", 0); + if (r >= 0) + return 0; + else + return (void*)(intptr_t)errno; +} + +/* + * Attempting to unmount the file system before it fully initializes should + * work fine. The unmount will complete after initialization does. + */ +TEST_F(PreInit, unmount_before_init) +{ + sem_t sem0; + pthread_t th1; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_INIT); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, init); + out.body.init.major = FUSE_KERNEL_VERSION; + out.body.init.minor = FUSE_KERNEL_MINOR_VERSION; + out.body.init.flags = in.body.init.flags & m_init_flags; + out.body.init.max_write = m_maxwrite; + out.body.init.max_readahead = m_maxreadahead; + out.body.init.time_gran = m_time_gran; + sem_wait(&sem0); + }))); + expect_destroy(0); + + ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL)); + nap(); /* Wait for th1 to block in unmount() */ + sem_post(&sem0); + /* The daemon will quit after receiving FUSE_DESTROY */ + m_mock->join_daemon(); +} + +/* + * Don't panic in this very specific scenario: + * + * The server does not respond to FUSE_INIT in timely fashion. + * Some other process tries to do unmount. + * That other process gets killed by a signal. + * The server finally responds to FUSE_INIT. + * + * Regression test for bug 287438 + * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438 + */ +TEST_F(PreInit, signal_during_unmount_before_init) +{ + sem_t sem0; + pid_t child; + + ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno); + + EXPECT_CALL(*m_mock, process( + ResultOf([](auto in) { + return (in.header.opcode == FUSE_INIT); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) { + SET_OUT_HEADER_LEN(out, init); + out.body.init.major = FUSE_KERNEL_VERSION; + /* + * Use protocol 7.19, like libfuse2 does. The server must use + * protocol 7.27 or older to trigger the bug. + */ + out.body.init.minor = 19; + out.body.init.flags = in.body.init.flags & m_init_flags; + out.body.init.max_write = m_maxwrite; + out.body.init.max_readahead = m_maxreadahead; + out.body.init.time_gran = m_time_gran; + sem_wait(&sem0); + }))); + + if ((child = ::fork()) == 0) { + /* + * In child. This will block waiting for FUSE_INIT to complete + * or the receipt of an asynchronous signal. + */ + (void) unmount("mountpoint", 0); + _exit(0); /* Unreachable, unless parent dies after fork */ + } else if (child > 0) { + /* In parent. Wait for child process to start, then kill it */ + nap(); + kill(child, SIGINT); + waitpid(child, NULL, WEXITED); + } else { + FAIL() << strerror(errno); + } + m_mock->m_quit = true; /* Since we are by now unmounted. */ + sem_post(&sem0); + m_mock->join_daemon(); +} diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc index 373f742d4fd3..e9c79ba2ffda 100644 --- a/tests/sys/fs/fusefs/read.cc +++ b/tests/sys/fs/fusefs/read.cc @@ -111,6 +111,13 @@ class ReadAhead: public Read, } }; +class ReadMaxRead: public Read { + virtual void SetUp() { + m_maxread = 16384; + Read::SetUp(); + } +}; + class ReadNoatime: public Read { virtual void SetUp() { m_noatime = true; @@ -840,6 +847,52 @@ TEST_F(Read, mmap) leak(fd); } + +/* When max_read is set, large reads will be split up as necessary */ +TEST_F(ReadMaxRead, split) +{ + const char FULLPATH[] = "mountpoint/some_file.txt"; + const char RELPATH[] = "some_file.txt"; + uint64_t ino = 42; + int fd; + ssize_t bufsize = 65536; + ssize_t fragsize = bufsize / 4; + char *rbuf, *frag0, *frag1, *frag2, *frag3; + + rbuf = new char[bufsize](); + frag0 = new char[fragsize](); + frag1 = new char[fragsize](); + frag2 = new char[fragsize](); + frag3 = new char[fragsize](); + memset(frag0, '0', fragsize); + memset(frag1, '1', fragsize); + memset(frag2, '2', fragsize); + memset(frag3, '3', fragsize); + + expect_lookup(RELPATH, ino, bufsize); + expect_open(ino, 0, 1); + expect_read(ino, 0, fragsize, fragsize, frag0); + expect_read(ino, fragsize, fragsize, fragsize, frag1); + expect_read(ino, 2 * fragsize, fragsize, fragsize, frag2); + expect_read(ino, 3 * fragsize, fragsize, fragsize, frag3); + + fd = open(FULLPATH, O_RDONLY); + ASSERT_LE(0, fd) << strerror(errno); + + ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno); + ASSERT_EQ(0, memcmp(rbuf, frag0, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + fragsize, frag1, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + 2 * fragsize, frag2, fragsize)); + ASSERT_EQ(0, memcmp(rbuf + 3 * fragsize, frag3, fragsize)); + + delete[] frag3; + delete[] frag2; + delete[] frag1; + delete[] frag0; + delete[] rbuf; + leak(fd); +} + /* * The kernel should not update the cached atime attribute during a read, if * MNT_NOATIME is used. @@ -1339,7 +1392,7 @@ TEST_P(ReadAhead, readahead) { expect_open(ino, 0, 1); maxcontig = m_noclusterr ? m_maxbcachebuf : m_maxbcachebuf + m_maxreadahead; - clustersize = MIN(maxcontig, m_maxphys); + clustersize = MIN((unsigned long )maxcontig, m_maxphys); for (offs = 0; offs < bufsize; offs += clustersize) { len = std::min((size_t)clustersize, (size_t)(filesize - offs)); expect_read(ino, offs, len, len, contents + offs); diff --git a/tests/sys/fs/fusefs/unlink.cc b/tests/sys/fs/fusefs/unlink.cc index db54e286d85d..1d8a371649ee 100644 --- a/tests/sys/fs/fusefs/unlink.cc +++ b/tests/sys/fs/fusefs/unlink.cc @@ -1,6 +1,5 @@ /*- * Copyright (c) 2019 The FreeBSD Foundation - * All rights reserved. * * This software was developed by BFF Storage Systems, LLC under sponsorship * from the FreeBSD Foundation. diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index 55a552e28eeb..125b7e2d6fc7 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -124,29 +124,21 @@ bool is_unsafe_aio_enabled(void) { class FuseEnv: public Environment { virtual void SetUp() { + check_environment(); } }; void FuseTest::SetUp() { const char *maxbcachebuf_node = "vfs.maxbcachebuf"; const char *maxphys_node = "kern.maxphys"; - int val = 0; - size_t size = sizeof(val); - - /* - * XXX check_environment should be called from FuseEnv::SetUp, but - * can't due to https://github.com/google/googletest/issues/2189 - */ - check_environment(); - if (IsSkipped()) - return; + size_t size; - ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &val, &size, NULL, 0)) - << strerror(errno); - m_maxbcachebuf = val; - ASSERT_EQ(0, sysctlbyname(maxphys_node, &val, &size, NULL, 0)) + size = sizeof(m_maxbcachebuf); + ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &m_maxbcachebuf, &size, + NULL, 0)) << strerror(errno); + size = sizeof(m_maxphys); + ASSERT_EQ(0, sysctlbyname(maxphys_node, &m_maxphys, &size, NULL, 0)) << strerror(errno); - m_maxphys = val; /* * Set the default max_write to a distinct value from MAXPHYS to catch * bugs that confuse the two. @@ -155,11 +147,12 @@ void FuseTest::SetUp() { m_maxwrite = MIN(libfuse_max_write, (uint32_t)m_maxphys / 2); try { - m_mock = new MockFS(m_maxreadahead, m_allow_other, + m_mock = new MockFS(m_maxread, m_maxreadahead, m_allow_other, m_default_permissions, m_push_symlinks_in, m_ro, m_pm, m_init_flags, m_kernel_minor_version, m_maxwrite, m_async, m_noclusterr, m_time_gran, - m_nointr, m_noatime, m_fsname, m_subtype); + m_nointr, m_noatime, m_fsname, m_subtype, + m_no_auto_init); /* * FUSE_ACCESS is called almost universally. Expecting it in * each test case would be super-annoying. Instead, set a diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh index 383f01ea4bfe..91bbba909672 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -55,6 +55,7 @@ bool is_unsafe_aio_enabled(void); extern const uint32_t libfuse_max_write; class FuseTest : public ::testing::Test { protected: + uint32_t m_maxread; uint32_t m_maxreadahead; uint32_t m_maxwrite; uint32_t m_init_flags; @@ -68,6 +69,7 @@ class FuseTest : public ::testing::Test { bool m_async; bool m_noclusterr; bool m_nointr; + bool m_no_auto_init; unsigned m_time_gran; MockFS *m_mock = NULL; const static uint64_t FH = 0xdeadbeef1a7ebabe; @@ -77,9 +79,10 @@ class FuseTest : public ::testing::Test { public: int m_maxbcachebuf; - int m_maxphys; + unsigned long m_maxphys; FuseTest(): + m_maxread(0), m_maxreadahead(0), m_maxwrite(0), m_init_flags(0), @@ -93,6 +96,7 @@ class FuseTest : public ::testing::Test { m_async(false), m_noclusterr(false), m_nointr(false), + m_no_auto_init(false), m_time_gran(1), m_fsname(""), m_subtype(""), diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc index f931f350a7c3..1fe2e3cc522d 100644 --- a/tests/sys/fs/fusefs/write.cc +++ b/tests/sys/fs/fusefs/write.cc @@ -179,12 +179,12 @@ class WriteCluster: public WriteBack { public: virtual void SetUp() { m_async = true; - m_maxwrite = 1 << 25; // Anything larger than MAXPHYS will suffice + m_maxwrite = UINT32_MAX; // Anything larger than MAXPHYS will suffice WriteBack::SetUp(); if (m_maxphys < 2 * DFLTPHYS) GTEST_SKIP() << "MAXPHYS must be at least twice DFLTPHYS" << " for this test"; - if (m_maxphys < 2 * m_maxbcachebuf) + if (m_maxphys < 2 * (unsigned long )m_maxbcachebuf) GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf" << " for this test"; } @@ -860,7 +860,8 @@ TEST_F(WriteMaxWrite, write) ssize_t halfbufsize, bufsize; halfbufsize = m_mock->m_maxwrite; - if (halfbufsize >= m_maxbcachebuf || halfbufsize >= m_maxphys) + if (halfbufsize >= m_maxbcachebuf || + (unsigned long )halfbufsize >= m_maxphys) GTEST_SKIP() << "Must lower m_maxwrite for this test"; bufsize = halfbufsize * 2; contents = new int[bufsize / sizeof(int)]; diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc index 7fa2a741e074..0ab203c96254 100644 --- a/tests/sys/fs/fusefs/xattr.cc +++ b/tests/sys/fs/fusefs/xattr.cc @@ -110,6 +110,8 @@ void expect_setxattr(uint64_t ino, const char *attr, const char *value, const char *v = a + strlen(a) + 1; return (in.header.opcode == FUSE_SETXATTR && in.header.nodeid == ino && + in.body.setxattr.size == (strlen(value) + 1) && + in.body.setxattr.setxattr_flags == 0 && 0 == strcmp(attr, a) && 0 == strcmp(value, v)); }, Eq(true)), @@ -119,6 +121,33 @@ void expect_setxattr(uint64_t ino, const char *attr, const char *value, }; +class Xattr_7_32:public FuseTest { +public: +virtual void SetUp() +{ + m_kernel_minor_version = 32; + FuseTest::SetUp(); +} + +void expect_setxattr_7_32(uint64_t ino, const char *attr, const char *value, + ProcessMockerT r) +{ + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + const char *a = (const char *)in.body.bytes + + FUSE_COMPAT_SETXATTR_IN_SIZE; + const char *v = a + strlen(a) + 1; + return (in.header.opcode == FUSE_SETXATTR && + in.header.nodeid == ino && + in.body.setxattr.size == (strlen(value) + 1) && + 0 == strcmp(attr, a) && + 0 == strcmp(value, v)); + }, Eq(true)), + _) + ).WillOnce(Invoke(r)); +} +}; + class Getxattr: public Xattr {}; class Listxattr: public Xattr {}; @@ -153,6 +182,7 @@ void TearDown() { class Removexattr: public Xattr {}; class Setxattr: public Xattr {}; +class Setxattr_7_32:public Xattr_7_32 {}; class RofsXattr: public Xattr { public: virtual void SetUp() { @@ -350,7 +380,7 @@ TEST_F(Listxattr, enotsup) * On Linux, however, the file system is supposed to return ERANGE if an * insufficiently large buffer is passed to listxattr(2). * - * fusefs(5) must guarantee the usual FreeBSD behavior. + * fusefs(4) must guarantee the usual FreeBSD behavior. */ TEST_F(Listxattr, erange) { @@ -569,7 +599,7 @@ TEST_F(Listxattr, size_only_race_smaller) })); expect_listxattr(ino, sizeof(attrs0), ReturnImmediate([&](auto in __unused, auto& out) { - strlcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); + memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1)); out.header.len = sizeof(fuse_out_header) + sizeof(attrs1); }) @@ -728,6 +758,7 @@ TEST_F(Removexattr, system) << strerror(errno); } + /* * If the filesystem returns ENOSYS, then it will be treated as a permanent * failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP @@ -815,6 +846,23 @@ TEST_F(Setxattr, system) ASSERT_EQ(value_len, r) << strerror(errno); } + +TEST_F(Setxattr_7_32, ok) +{ + uint64_t ino = 42; + const char value[] = "whatever"; + ssize_t value_len = strlen(value) + 1; + int ns = EXTATTR_NAMESPACE_USER; + ssize_t r; + + expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1); + expect_setxattr_7_32(ino, "user.foo", value, ReturnErrno(0)); + + r = extattr_set_file(FULLPATH, ns, "foo", (const void *)value, + value_len); + ASSERT_EQ(value_len, r) << strerror(errno); +} + TEST_F(RofsXattr, deleteextattr_erofs) { uint64_t ino = 42; diff --git a/tests/sys/fs/tarfs/tarfs_test.sh b/tests/sys/fs/tarfs/tarfs_test.sh index f1322033fbad..20baadfea5c5 100644 --- a/tests/sys/fs/tarfs/tarfs_test.sh +++ b/tests/sys/fs/tarfs/tarfs_test.sh @@ -48,7 +48,6 @@ tarsum() { } tarfs_setup() { - kldload -n tarfs || atf_skip "This test requires tarfs and could not load it" mkdir "${mnt}" } @@ -60,6 +59,7 @@ atf_test_case tarfs_basic cleanup tarfs_basic_head() { atf_set "descr" "Basic function test" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_basic_body() { tarfs_setup @@ -87,6 +87,7 @@ atf_test_case tarfs_basic_gnu cleanup tarfs_basic_gnu_head() { atf_set "descr" "Basic function test using GNU tar" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "gtar" } tarfs_basic_gnu_body() { @@ -101,6 +102,7 @@ atf_test_case tarfs_notdir_device cleanup tarfs_notdir_device_head() { atf_set "descr" "Regression test for PR 269519 and 269561" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_notdir_device_body() { tarfs_setup @@ -121,6 +123,7 @@ atf_test_case tarfs_notdir_device_gnu cleanup tarfs_notdir_device_gnu_head() { atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "gtar" } tarfs_notdir_device_gnu_body() { @@ -135,6 +138,7 @@ atf_test_case tarfs_notdir_dot cleanup tarfs_notdir_dot_head() { atf_set "descr" "Regression test for PR 269519 and 269561" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_notdir_dot_body() { tarfs_setup @@ -155,6 +159,7 @@ atf_test_case tarfs_notdir_dot_gnu cleanup tarfs_notdir_dot_gnu_head() { atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "gtar" } tarfs_notdir_dot_gnu_body() { @@ -169,6 +174,7 @@ atf_test_case tarfs_notdir_dotdot cleanup tarfs_notdir_dotdot_head() { atf_set "descr" "Regression test for PR 269519 and 269561" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_notdir_dotdot_body() { tarfs_setup @@ -189,6 +195,7 @@ atf_test_case tarfs_notdir_dotdot_gnu cleanup tarfs_notdir_dotdot_gnu_head() { atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "gtar" } tarfs_notdir_dotdot_gnu_body() { @@ -203,6 +210,7 @@ atf_test_case tarfs_notdir_file cleanup tarfs_notdir_file_head() { atf_set "descr" "Regression test for PR 269519 and 269561" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_notdir_file_body() { tarfs_setup @@ -223,6 +231,7 @@ atf_test_case tarfs_notdir_file_gnu cleanup tarfs_notdir_file_gnu_head() { atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "gtar" } tarfs_notdir_file_gnu_body() { @@ -237,6 +246,7 @@ atf_test_case tarfs_emptylink cleanup tarfs_emptylink_head() { atf_set "descr" "Regression test for PR 277360: empty link target" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_emptylink_body() { tarfs_setup @@ -256,6 +266,7 @@ atf_test_case tarfs_linktodir cleanup tarfs_linktodir_head() { atf_set "descr" "Regression test for PR 277360: link to directory" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_linktodir_body() { tarfs_setup @@ -276,6 +287,7 @@ atf_test_case tarfs_linktononexistent cleanup tarfs_linktononexistent_head() { atf_set "descr" "Regression test for PR 277360: link to nonexistent target" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_linktononexistent_body() { tarfs_setup @@ -293,6 +305,7 @@ atf_test_case tarfs_checksum cleanup tarfs_checksum_head() { atf_set "descr" "Verify that the checksum covers header padding" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_checksum_body() { tarfs_setup @@ -313,6 +326,7 @@ atf_test_case tarfs_long_names cleanup tarfs_long_names_head() { atf_set "descr" "Verify that tarfs supports long file names" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_long_names_body() { tarfs_setup @@ -337,6 +351,7 @@ atf_test_case tarfs_long_paths cleanup tarfs_long_paths_head() { atf_set "descr" "Verify that tarfs supports long paths" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" } tarfs_long_paths_body() { tarfs_setup @@ -361,6 +376,7 @@ atf_test_case tarfs_git_archive cleanup tarfs_git_archive_head() { atf_set "descr" "Verify that tarfs supports archives created by git" atf_set "require.user" "root" + atf_set "require.kmods" "tarfs" atf_set "require.progs" "git" } tarfs_git_archive_body() { diff --git a/tests/sys/fs/tmpfs/Makefile b/tests/sys/fs/tmpfs/Makefile index 106b28c7c463..ce4ca959838c 100644 --- a/tests/sys/fs/tmpfs/Makefile +++ b/tests/sys/fs/tmpfs/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests FILESYSTEM?= ${.CURDIR:T} diff --git a/tests/sys/geom/Makefile b/tests/sys/geom/Makefile index 7e6e7cd2bb65..78257e180cf9 100644 --- a/tests/sys/geom/Makefile +++ b/tests/sys/geom/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/geom TESTS_SUBDIRS+= class diff --git a/tests/sys/geom/class/Makefile b/tests/sys/geom/class/Makefile index 10b01a043ddf..3cf3a15273ac 100644 --- a/tests/sys/geom/class/Makefile +++ b/tests/sys/geom/class/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests @@ -19,6 +18,7 @@ TESTS_SUBDIRS+= shsec TESTS_SUBDIRS+= stripe TESTS_SUBDIRS+= union TESTS_SUBDIRS+= uzip +TESTS_SUBDIRS+= virstor ${PACKAGE}FILES+= geom_subr.sh diff --git a/tests/sys/geom/class/Makefile.inc b/tests/sys/geom/class/Makefile.inc index 03efce15f856..cec69b26e149 100644 --- a/tests/sys/geom/class/Makefile.inc +++ b/tests/sys/geom/class/Makefile.inc @@ -1,2 +1 @@ - .include "${SRCTOP}/tests/Makefile.inc0" diff --git a/tests/sys/geom/class/concat/Makefile b/tests/sys/geom/class/concat/Makefile index 335c7632c6e5..d5457b64bd2a 100644 --- a/tests/sys/geom/class/concat/Makefile +++ b/tests/sys/geom/class/concat/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/eli/Makefile b/tests/sys/geom/class/eli/Makefile index d8faff182ac4..e1a28f39d5d2 100644 --- a/tests/sys/geom/class/eli/Makefile +++ b/tests/sys/geom/class/eli/Makefile @@ -1,4 +1,3 @@ - .PATH: ${SRCTOP}/sys/geom/eli ${SRCTOP}/sys/crypto/sha2 PACKAGE= tests diff --git a/tests/sys/geom/class/eli/attach_test.sh b/tests/sys/geom/class/eli/attach_test.sh index eb59234f014b..b6b1848f2d37 100644 --- a/tests/sys/geom/class/eli/attach_test.sh +++ b/tests/sys/geom/class/eli/attach_test.sh @@ -39,6 +39,34 @@ attach_d_cleanup() geli_test_cleanup } +atf_test_case atach_multiple_fails cleanup +attach_multiple_fails_head() +{ + atf_set "descr" "test multiple failed attach of geli provider" + atf_set "require.user" "root" +} +attach_multiple_fails_body() +{ + geli_test_setup + + sectors=1000 + attach_md md -t malloc -s `expr $sectors + 1` + atf_check dd if=/dev/random of=keyfile bs=512 count=16 status=none + + atf_check geli init -B none -P -K keyfile ${md} + atf_check geli attach -d -p -k keyfile ${md} + + for i in $(jot 100); do + atf_check -s not-exit:0 -e ignore -- geli attach -d -p -k keyfile ${md} + done + atf_check -o ignore -- newfs ${md}.eli +} +attach_multiple_fails_cleanup() +{ + geli_test_cleanup +} + + atf_test_case attach_r cleanup attach_r_head() { @@ -125,5 +153,6 @@ atf_init_test_cases() atf_add_test_case attach_d atf_add_test_case attach_r atf_add_test_case attach_multiple + atf_add_test_case attach_multiple_fails atf_add_test_case nokey } diff --git a/tests/sys/geom/class/gate/Makefile b/tests/sys/geom/class/gate/Makefile index a51c983f5cf6..5ebc1d9304e0 100644 --- a/tests/sys/geom/class/gate/Makefile +++ b/tests/sys/geom/class/gate/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/gate/ggate_test.sh b/tests/sys/geom/class/gate/ggate_test.sh index 3ca5c3a2531a..4cbc5d80ae89 100644 --- a/tests/sys/geom/class/gate/ggate_test.sh +++ b/tests/sys/geom/class/gate/ggate_test.sh @@ -1,7 +1,5 @@ - PIDFILE=ggated.pid PLAINFILES=plainfiles -PORT=33080 CONF=gg.exports atf_test_case ggatec_trim cleanup @@ -17,13 +15,14 @@ ggatec_trim_body() { load_ggate + port=33080 us=$(alloc_ggate_dev) work=$(alloc_md) atf_check -e ignore -o ignore dd if=/dev/random of=/dev/$work bs=1m count=1 conv=notrunc echo $CONF >> $PLAINFILES echo "localhost RW /dev/$work" > $CONF - atf_check ggated -p $PORT -F $PIDFILE $CONF - atf_check ggatec create -p $PORT -u $us localhost /dev/$work + atf_check ggated -p $port -F $PIDFILE $CONF + atf_check ggatec create -p $port -u $us localhost /dev/$work ggate_dev=/dev/ggate${us} wait_for_ggate_device ${ggate_dev} @@ -55,6 +54,7 @@ ggated_body() load_ggate + port=33081 us=$(alloc_ggate_dev) work=$(alloc_md) src=$(alloc_md) @@ -67,8 +67,8 @@ ggated_body() echo $CONF >> $PLAINFILES echo "127.0.0.1 RW /dev/$work" > $CONF - atf_check ggated -p $PORT -F $PIDFILE $CONF - atf_check ggatec create -p $PORT -u $us 127.0.0.1 /dev/$work + atf_check ggated -p $port -F $PIDFILE $CONF + atf_check ggatec create -p $port -u $us 127.0.0.1 /dev/$work ggate_dev=/dev/ggate${us} diff --git a/tests/sys/geom/class/mirror/Makefile b/tests/sys/geom/class/mirror/Makefile index fdef410ab974..635288cc3d53 100644 --- a/tests/sys/geom/class/mirror/Makefile +++ b/tests/sys/geom/class/mirror/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/multipath/Makefile b/tests/sys/geom/class/multipath/Makefile index 9a2a9a29ec38..1246015e8fc4 100644 --- a/tests/sys/geom/class/multipath/Makefile +++ b/tests/sys/geom/class/multipath/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/nop/Makefile b/tests/sys/geom/class/nop/Makefile index 54b41584d441..e2cb249bcf56 100644 --- a/tests/sys/geom/class/nop/Makefile +++ b/tests/sys/geom/class/nop/Makefile @@ -1,8 +1,11 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} ATF_TESTS_SH+= nop_test +# Some tests make use of the "disks" property and kyua may schedule +# them to run at the time time, which the tests do not expect. +TEST_METADATA.nop_test+= is_exclusive="true" + .include <bsd.test.mk> diff --git a/tests/sys/geom/class/part/Makefile b/tests/sys/geom/class/part/Makefile index 6ca33655c805..0d7d86224437 100644 --- a/tests/sys/geom/class/part/Makefile +++ b/tests/sys/geom/class/part/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/raid3/Makefile b/tests/sys/geom/class/raid3/Makefile index 97b9cd34b8f0..8f398a0f5fec 100644 --- a/tests/sys/geom/class/raid3/Makefile +++ b/tests/sys/geom/class/raid3/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/shsec/Makefile b/tests/sys/geom/class/shsec/Makefile index 7615142f605f..500c14cca51a 100644 --- a/tests/sys/geom/class/shsec/Makefile +++ b/tests/sys/geom/class/shsec/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/stripe/Makefile b/tests/sys/geom/class/stripe/Makefile index 7615142f605f..500c14cca51a 100644 --- a/tests/sys/geom/class/stripe/Makefile +++ b/tests/sys/geom/class/stripe/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} diff --git a/tests/sys/geom/class/virstor/Makefile b/tests/sys/geom/class/virstor/Makefile new file mode 100644 index 000000000000..67242879e33f --- /dev/null +++ b/tests/sys/geom/class/virstor/Makefile @@ -0,0 +1,9 @@ +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T} + +ATF_TESTS_SH+= virstor_test + +${PACKAGE}FILES+= conf.sh + +.include <bsd.test.mk> diff --git a/tests/sys/geom/class/virstor/conf.sh b/tests/sys/geom/class/virstor/conf.sh new file mode 100644 index 000000000000..46b0fd1308a3 --- /dev/null +++ b/tests/sys/geom/class/virstor/conf.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +class="virstor" +base=$(atf_get ident) +TEST_VIRSTOR_DEVS_FILE="${TMPDIR}/test_virstor_devs.$(basename $0)" + +gvirstor_dev_setup() +{ + # Pick a random name and record it for cleanup. + local vdevbase="$(mktemp -u virstor.XXXXXX)" || aft_fail "mktemp" + echo "$vdevbase" >> "$TEST_VIRSTOR_DEVS_FILE" + eval "${1}='${vdevbase}'" +} + +gvirstor_test_cleanup() +{ + local vdevbase + if [ -f "$TEST_VIRSTOR_DEVS_FILE" ]; then + while read vdevbase; do + if [ -c "/dev/$class/$vdevbase" ]; then + echo "# Destroying test virstor device:" \ + "$vdevbase" + gvirstor destroy "$vdevbase" + fi + done < "$TEST_VIRSTOR_DEVS_FILE" + fi + geom_test_cleanup +} + +ATF_TEST=true +. `dirname $0`/../geom_subr.sh diff --git a/tests/sys/geom/class/virstor/virstor_test.sh b/tests/sys/geom/class/virstor/virstor_test.sh new file mode 100644 index 000000000000..4f2047bffe97 --- /dev/null +++ b/tests/sys/geom/class/virstor/virstor_test.sh @@ -0,0 +1,73 @@ +# +# Copyright (c) 2024 Dell Inc. or its subsidiaries. All Rights Reserved. +# +# SPDX-License-Identifier: BSD-2-Clause +# + +. $(atf_get_srcdir)/conf.sh + +atf_test_case basic cleanup +basic_head() +{ + atf_set "descr" "geom virstor basic functional test" + atf_set "require.user" "root" +} +basic_body() +{ + geom_atf_test_setup + # Choose a virstor device name + gvirstor_dev_setup name + + # Create an md backing device and initialize it with junk + psecsize=512 + attach_md md -t swap -S $psecsize -s 5M || atf_fail "attach_md" + jot -b uninitialized 0 | dd status=none of=/dev/$md 2> /dev/null + + # Create a virstor device + vsizemb=64 + vsize=$((vsizemb * 1024 * 1024)) + atf_check -o ignore -e ignore \ + gvirstor label -v -s ${vsizemb}M -m 512 $name /dev/$md + devwait + vdev="/dev/$class/$name" + + ssize=$(diskinfo $vdev | awk '{print $2}') + atf_check_equal $psecsize $ssize + + size=$(diskinfo $vdev | awk '{print $3}') + atf_check_equal $vsize $size + + # Write the first and last sectors of the virtual address space + hasha=$(jot -b a 0 | head -c $ssize | sha1) + hashz=$(jot -b z 0 | head -c $ssize | sha1) + zsector=$((vsize / ssize - 1)) + jot -b a 0 | dd status=none of=$vdev bs=$ssize count=1 conv=notrunc + jot -b z 0 | dd status=none of=$vdev bs=$ssize count=1 conv=notrunc \ + seek=$zsector + + # Read back and compare + hashx=$(dd status=none if=$vdev bs=$ssize count=1 | sha1) + atf_check_equal $hasha $hashx + hashx=$(dd status=none if=$vdev bs=$ssize count=1 skip=$zsector | sha1) + atf_check_equal $hashz $hashx + + # Destroy, then retaste and reload + atf_check -o ignore gvirstor destroy $name + true > /dev/$md + devwait + + # Read back and compare + hashx=$(dd status=none if=$vdev bs=$ssize count=1 | sha1) + atf_check_equal $hasha $hashx + hashx=$(dd status=none if=$vdev bs=$ssize count=1 skip=$zsector | sha1) + atf_check_equal $hashz $hashx +} +basic_cleanup() +{ + gvirstor_test_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case basic +} diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile index c5107ce8f9fe..26c0013696c7 100644 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> PACKAGE= tests @@ -16,7 +15,11 @@ ATF_TESTS_C+= kcov .endif ATF_TESTS_C+= kern_copyin ATF_TESTS_C+= kern_descrip_test +# One test modifies the maxfiles limit, which can cause spurious test failures. +TEST_METADATA.kern_descrip_test+= is_exclusive="true" ATF_TESTS_C+= fdgrowtable_test +ATF_TESTS_C+= jail_lookup_root +ATF_TESTS_C+= inotify_test ATF_TESTS_C+= kill_zombie .if ${MK_OPENSSL} != "no" ATF_TESTS_C+= ktls_test @@ -24,6 +27,7 @@ ATF_TESTS_C+= ktls_test ATF_TESTS_C+= ktrace_test ATF_TESTS_C+= listener_wakeup ATF_TESTS_C+= module_test +ATF_TESTS_C+= prace ATF_TESTS_C+= ptrace_test TEST_METADATA.ptrace_test+= timeout="15" ATF_TESTS_C+= reaper @@ -35,6 +39,7 @@ ATF_TESTS_C+= socket_accept ATF_TESTS_C+= socket_accf ATF_TESTS_C+= socket_msg_trunc ATF_TESTS_C+= socket_msg_waitall +ATF_TESTS_C+= socket_splice TEST_METADATA.sigwait+= is_exclusive="true" .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH:Mpowerpc*} == "" ATF_TESTS_C+= subr_physmem_test @@ -57,12 +62,14 @@ ATF_TESTS_C+= sigsys TEST_METADATA.sigsys+= is_exclusive="true" ATF_TESTS_SH+= coredump_phnum_test +ATF_TESTS_SH+= logsigexit_test +ATF_TESTS_SH+= jailmeta ATF_TESTS_SH+= sonewconn_overflow TEST_METADATA.sonewconn_overflow+= required_programs="python" TEST_METADATA.sonewconn_overflow+= required_user="root" +TEST_METADATA.sonewconn_overflow+= is_exclusive="true" ATF_TESTS_SH+= sendfile_test ATF_TESTS_SH+= sysctl_security_jail_children -TEST_METADATA.sysctl_security_jail_children+= is_exclusive="true" ${PACKAGE}FILES+= sonewconn_overflow.py ${PACKAGE}FILESMODE_sonewconn_overflow.py=0555 @@ -72,22 +79,28 @@ PROGS+= coredump_phnum_helper PROGS+= pdeathsig_helper PROGS+= sendfile_helper +LIBADD.jail_lookup_root+= jail util CFLAGS.sys_getrandom+= -I${SRCTOP}/sys/contrib/zstd/lib LIBADD.sys_getrandom+= zstd LIBADD.sys_getrandom+= c LIBADD.sys_getrandom+= pthread LIBADD.ptrace_test+= pthread LIBADD.unix_seqpacket_test+= pthread +LIBADD.inotify_test+= util LIBADD.kcov+= pthread CFLAGS.ktls_test+= -DOPENSSL_API_COMPAT=0x10100000L LIBADD.ktls_test+= crypto util LIBADD.listener_wakeup+= pthread LIBADD.shutdown_dgram+= pthread LIBADD.socket_msg_waitall+= pthread +LIBADD.socket_splice+= pthread LIBADD.sendfile_helper+= pthread LIBADD.fdgrowtable_test+= util pthread kvm procstat LIBADD.sigwait+= rt LIBADD.ktrace_test+= sysdecode +LIBADD.unix_passfd_dgram+= jail +LIBADD.unix_passfd_stream+= jail +LIBADD.unix_stream+= pthread NETBSD_ATF_TESTS_C+= lockf_test NETBSD_ATF_TESTS_C+= mqueue_test @@ -125,6 +138,7 @@ WARNS?= 3 TESTS_SUBDIRS+= acct TESTS_SUBDIRS+= execve TESTS_SUBDIRS+= pipe +TESTS_SUBDIRS+= tty .include <netbsd-tests.test.mk> diff --git a/tests/sys/kern/Makefile.inc b/tests/sys/kern/Makefile.inc index d3b5cbd3a79b..01b5f23410c8 100644 --- a/tests/sys/kern/Makefile.inc +++ b/tests/sys/kern/Makefile.inc @@ -1,2 +1 @@ - .include "../Makefile.inc" diff --git a/tests/sys/kern/acct/Makefile b/tests/sys/kern/acct/Makefile index 6f1181fe6d8a..779be0ae591d 100644 --- a/tests/sys/kern/acct/Makefile +++ b/tests/sys/kern/acct/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/kern/acct ATF_TESTS_C= acct_test diff --git a/tests/sys/kern/coredump_phnum_test.sh b/tests/sys/kern/coredump_phnum_test.sh index e01c5d86a252..a1337d5ad1fb 100644 --- a/tests/sys/kern/coredump_phnum_test.sh +++ b/tests/sys/kern/coredump_phnum_test.sh @@ -80,6 +80,11 @@ coredump_phnum_body() atf_fail "Helper program did not dump core" fi + if readelf --version | grep -q LLVM; then + atf_expect_fail "PR285547: llvm-objdump does not support large phdr count" + # See https://github.com/llvm/llvm-project/issues/132216 + fi + # These magic numbers don't have any real significance. They are just # the result of running the helper program and dumping core. The only # important bit is that they're larger than 65535 (UINT16_MAX). diff --git a/tests/sys/kern/execve/Makefile b/tests/sys/kern/execve/Makefile index 77221b2d2dfa..afa537031c39 100644 --- a/tests/sys/kern/execve/Makefile +++ b/tests/sys/kern/execve/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/kern/execve BINDIR= ${TESTSDIR} diff --git a/tests/sys/kern/inotify_test.c b/tests/sys/kern/inotify_test.c new file mode 100644 index 000000000000..ed7cef5d148c --- /dev/null +++ b/tests/sys/kern/inotify_test.c @@ -0,0 +1,862 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Klara, Inc. + */ + +#include <sys/capsicum.h> +#include <sys/filio.h> +#include <sys/inotify.h> +#include <sys/ioccom.h> +#include <sys/mount.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/sysctl.h> +#include <sys/un.h> + +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <mntopts.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <atf-c.h> + +static const char * +ev2name(int event) +{ + switch (event) { + case IN_ACCESS: + return ("IN_ACCESS"); + case IN_ATTRIB: + return ("IN_ATTRIB"); + case IN_CLOSE_WRITE: + return ("IN_CLOSE_WRITE"); + case IN_CLOSE_NOWRITE: + return ("IN_CLOSE_NOWRITE"); + case IN_CREATE: + return ("IN_CREATE"); + case IN_DELETE: + return ("IN_DELETE"); + case IN_DELETE_SELF: + return ("IN_DELETE_SELF"); + case IN_MODIFY: + return ("IN_MODIFY"); + case IN_MOVE_SELF: + return ("IN_MOVE_SELF"); + case IN_MOVED_FROM: + return ("IN_MOVED_FROM"); + case IN_MOVED_TO: + return ("IN_MOVED_TO"); + case IN_OPEN: + return ("IN_OPEN"); + default: + return (NULL); + } +} + +static void +close_checked(int fd) +{ + ATF_REQUIRE(close(fd) == 0); +} + +/* + * Make sure that no other events are pending, and close the inotify descriptor. + */ +static void +close_inotify(int fd) +{ + int n; + + ATF_REQUIRE(ioctl(fd, FIONREAD, &n) == 0); + ATF_REQUIRE(n == 0); + close_checked(fd); +} + +static uint32_t +consume_event_cookie(int ifd, int wd, int event, int flags, const char *name) +{ + struct inotify_event *ev; + size_t evsz, namelen; + ssize_t n; + uint32_t cookie; + + /* Only read one record. */ + namelen = name == NULL ? 0 : strlen(name); + evsz = sizeof(*ev) + _IN_NAMESIZE(namelen); + ev = malloc(evsz); + ATF_REQUIRE(ev != NULL); + + n = read(ifd, ev, evsz); + ATF_REQUIRE_MSG(n >= 0, "failed to read event %s", ev2name(event)); + ATF_REQUIRE((size_t)n >= sizeof(*ev)); + ATF_REQUIRE((size_t)n == sizeof(*ev) + ev->len); + ATF_REQUIRE((size_t)n == evsz); + + ATF_REQUIRE_MSG((ev->mask & IN_ALL_EVENTS) == event, + "expected event %#x, got %#x", event, ev->mask); + ATF_REQUIRE_MSG((ev->mask & _IN_ALL_RETFLAGS) == flags, + "expected flags %#x, got %#x", flags, ev->mask); + ATF_REQUIRE_MSG(ev->wd == wd, + "expected wd %d, got %d", wd, ev->wd); + ATF_REQUIRE_MSG(name == NULL || strcmp(name, ev->name) == 0, + "expected name '%s', got '%s'", name, ev->name); + cookie = ev->cookie; + if ((ev->mask & (IN_MOVED_FROM | IN_MOVED_TO)) == 0) + ATF_REQUIRE(cookie == 0); + free(ev); + return (cookie); +} + +/* + * Read an event from the inotify file descriptor and check that it + * matches the expected values. + */ +static void +consume_event(int ifd, int wd, int event, int flags, const char *name) +{ + (void)consume_event_cookie(ifd, wd, event, flags, name); +} + +static int +inotify(int flags) +{ + int ifd; + + ifd = inotify_init1(flags); + ATF_REQUIRE(ifd != -1); + return (ifd); +} + +static void +mount_nullfs(char *dir, char *src) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, "fstype", "nullfs", (size_t)-1); + build_iovec(&iov, &iovlen, "fspath", dir, (size_t)-1); + build_iovec(&iov, &iovlen, "target", src, (size_t)-1); + build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, + "mount nullfs %s %s: %s", src, dir, + errmsg[0] == '\0' ? strerror(errno) : errmsg); + + free_iovec(&iov, &iovlen); +} + +static void +mount_tmpfs(const char *dir) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, "fstype", "tmpfs", (size_t)-1); + build_iovec(&iov, &iovlen, "fspath", __DECONST(char *, dir), + (size_t)-1); + build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, + "mount tmpfs %s: %s", dir, + errmsg[0] == '\0' ? strerror(errno) : errmsg); + + free_iovec(&iov, &iovlen); +} + +static int +watch_file(int ifd, int events, char *path) +{ + int fd, wd; + + strncpy(path, "test.XXXXXX", PATH_MAX); + fd = mkstemp(path); + ATF_REQUIRE(fd != -1); + close_checked(fd); + + wd = inotify_add_watch(ifd, path, events); + ATF_REQUIRE(wd != -1); + + return (wd); +} + +static int +watch_dir(int ifd, int events, char *path) +{ + char *p; + int wd; + + strlcpy(path, "test.XXXXXX", PATH_MAX); + p = mkdtemp(path); + ATF_REQUIRE(p == path); + + wd = inotify_add_watch(ifd, path, events); + ATF_REQUIRE(wd != -1); + + return (wd); +} + +/* + * Verify that Capsicum restrictions are applied as expected. + */ +ATF_TC_WITHOUT_HEAD(inotify_capsicum); +ATF_TC_BODY(inotify_capsicum, tc) +{ + int error, dfd, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + ATF_REQUIRE(ifd != -1); + + dfd = open(".", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + + error = mkdirat(dfd, "testdir", 0755); + ATF_REQUIRE(error == 0); + + error = cap_enter(); + ATF_REQUIRE(error == 0); + + /* + * Plain inotify_add_watch() is disallowed. + */ + wd = inotify_add_watch(ifd, ".", IN_DELETE_SELF); + ATF_REQUIRE_ERRNO(ECAPMODE, wd == -1); + wd = inotify_add_watch_at(ifd, dfd, "testdir", IN_DELETE_SELF); + ATF_REQUIRE(wd >= 0); + + /* + * Generate a record and consume it. + */ + error = unlinkat(dfd, "testdir", AT_REMOVEDIR); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_checked(dfd); + close_inotify(ifd); +} + +/* + * Make sure that duplicate, back-to-back events are coalesced. + */ +ATF_TC_WITHOUT_HEAD(inotify_coalesce); +ATF_TC_BODY(inotify_coalesce, tc) +{ + char file[PATH_MAX], path[PATH_MAX]; + int fd, fd1, ifd, n, wd; + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it. */ + wd = watch_dir(ifd, IN_OPEN, path); + /* Create a file in the directory and open it. */ + snprintf(file, sizeof(file), "%s/file", path); + fd = open(file, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + fd = open(file, O_RDWR); + ATF_REQUIRE(fd != -1); + fd1 = open(file, O_RDONLY); + ATF_REQUIRE(fd1 != -1); + close_checked(fd1); + close_checked(fd); + + consume_event(ifd, wd, IN_OPEN, 0, "file"); + ATF_REQUIRE(ioctl(ifd, FIONREAD, &n) == 0); + ATF_REQUIRE(n == 0); + + close_inotify(ifd); +} + +/* + * Check handling of IN_MASK_CREATE. + */ +ATF_TC_WITHOUT_HEAD(inotify_mask_create); +ATF_TC_BODY(inotify_mask_create, tc) +{ + char path[PATH_MAX]; + int ifd, wd, wd1; + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it. */ + wd = watch_dir(ifd, IN_CREATE, path); + /* Updating the watch with IN_MASK_CREATE should result in an error. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_CREATE); + ATF_REQUIRE_ERRNO(EEXIST, wd1 == -1); + /* It's an error to specify IN_MASK_ADD with IN_MASK_CREATE. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_ADD | + IN_MASK_CREATE); + ATF_REQUIRE_ERRNO(EINVAL, wd1 == -1); + /* Updating the watch without IN_MASK_CREATE should work. */ + wd1 = inotify_add_watch(ifd, path, IN_MODIFY); + ATF_REQUIRE(wd1 != -1); + ATF_REQUIRE_EQ(wd, wd1); + + close_inotify(ifd); +} + +/* + * Make sure that inotify cooperates with nullfs: if a lower vnode is the + * subject of an event, the upper vnode should be notified, and if the upper + * vnode is the subject of an event, the lower vnode should be notified. + */ +ATF_TC_WITH_CLEANUP(inotify_nullfs); +ATF_TC_HEAD(inotify_nullfs, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(inotify_nullfs, tc) +{ + char path[PATH_MAX], *p; + int dfd, error, fd, ifd, mask, wd; + + mask = IN_CREATE | IN_OPEN; + + ifd = inotify(IN_NONBLOCK); + + strlcpy(path, "./test.XXXXXX", sizeof(path)); + p = mkdtemp(path); + ATF_REQUIRE(p == path); + + error = mkdir("./mnt", 0755); + ATF_REQUIRE(error == 0); + + /* Mount the testdir onto ./mnt. */ + mount_nullfs("./mnt", path); + + wd = inotify_add_watch(ifd, "./mnt", mask); + ATF_REQUIRE(wd != -1); + + /* Create a file in the lower directory and open it. */ + dfd = open(path, O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + fd = openat(dfd, "file", O_RDWR | O_CREAT, 0644); + close_checked(fd); + close_checked(dfd); + + /* We should see events via the nullfs mount. */ + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + consume_event(ifd, wd, IN_CREATE, 0, "file"); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + error = inotify_rm_watch(ifd, wd); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + /* Watch the lower directory. */ + wd = inotify_add_watch(ifd, path, mask); + ATF_REQUIRE(wd != -1); + /* ... and create a file in the upper directory and open it. */ + dfd = open("./mnt", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + fd = openat(dfd, "file2", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + close_checked(dfd); + + /* We should see events via the lower directory. */ + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + consume_event(ifd, wd, IN_CREATE, 0, "file2"); + consume_event(ifd, wd, IN_OPEN, 0, "file2"); + + close_inotify(ifd); +} +ATF_TC_CLEANUP(inotify_nullfs, tc) +{ + int error; + + error = unmount("./mnt", 0); + if (error != 0) { + perror("unmount"); + exit(1); + } +} + +/* + * Make sure that exceeding max_events pending events results in an overflow + * event. + */ +ATF_TC_WITHOUT_HEAD(inotify_queue_overflow); +ATF_TC_BODY(inotify_queue_overflow, tc) +{ + char path[PATH_MAX]; + size_t size; + int error, dfd, ifd, max, wd; + + size = sizeof(max); + error = sysctlbyname("vfs.inotify.max_queued_events", &max, &size, NULL, + 0); + ATF_REQUIRE(error == 0); + + ifd = inotify(IN_NONBLOCK); + + /* Create a directory and watch it for file creation events. */ + wd = watch_dir(ifd, IN_CREATE, path); + dfd = open(path, O_DIRECTORY); + ATF_REQUIRE(dfd != -1); + /* Generate max+1 file creation events. */ + for (int i = 0; i < max + 1; i++) { + char name[NAME_MAX]; + int fd; + + (void)snprintf(name, sizeof(name), "file%d", i); + fd = openat(dfd, name, O_CREAT | O_RDWR, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + } + + /* + * Read our events. We should see files 0..max-1 and then an overflow + * event. + */ + for (int i = 0; i < max; i++) { + char name[NAME_MAX]; + + (void)snprintf(name, sizeof(name), "file%d", i); + consume_event(ifd, wd, IN_CREATE, 0, name); + } + + /* Look for an overflow event. */ + consume_event(ifd, -1, 0, IN_Q_OVERFLOW, NULL); + + close_checked(dfd); + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_access_file); +ATF_TC_BODY(inotify_event_access_file, tc) +{ + char path[PATH_MAX], buf[16]; + off_t nb; + ssize_t n; + int error, fd, fd1, ifd, s[2], wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_ACCESS, path); + + fd = open(path, O_RDWR); + n = write(fd, "test", 4); + ATF_REQUIRE(n == 4); + + /* A simple read(2) should generate an access. */ + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + n = read(fd, buf, sizeof(buf)); + ATF_REQUIRE(n == 4); + ATF_REQUIRE(memcmp(buf, "test", 4) == 0); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + + /* copy_file_range(2) should as well. */ + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + fd1 = open("sink", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd1 != -1); + n = copy_file_range(fd, NULL, fd1, NULL, 4, 0); + ATF_REQUIRE(n == 4); + close_checked(fd1); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + + /* As should sendfile(2). */ + error = socketpair(AF_UNIX, SOCK_STREAM, 0, s); + ATF_REQUIRE(error == 0); + error = sendfile(fd, s[0], 0, 4, NULL, &nb, 0); + ATF_REQUIRE(error == 0); + ATF_REQUIRE(nb == 4); + consume_event(ifd, wd, IN_ACCESS, 0, NULL); + close_checked(s[0]); + close_checked(s[1]); + + close_checked(fd); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_access_dir); +ATF_TC_BODY(inotify_event_access_dir, tc) +{ + char root[PATH_MAX], path[PATH_MAX]; + struct dirent *ent; + DIR *dir; + int error, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_ACCESS, root); + snprintf(path, sizeof(path), "%s/dir", root); + error = mkdir(path, 0755); + ATF_REQUIRE(error == 0); + + /* Read an entry and generate an access. */ + dir = opendir(path); + ATF_REQUIRE(dir != NULL); + ent = readdir(dir); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0); + ATF_REQUIRE(closedir(dir) == 0); + consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, "dir"); + + /* + * Reading the watched directory should generate an access event. + * This is contrary to Linux's inotify man page, which states that + * IN_ACCESS is only generated for accesses to objects in a watched + * directory. + */ + dir = opendir(root); + ATF_REQUIRE(dir != NULL); + ent = readdir(dir); + ATF_REQUIRE(ent != NULL); + ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0); + ATF_REQUIRE(closedir(dir) == 0); + consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_attrib); +ATF_TC_BODY(inotify_event_attrib, tc) +{ + char path[PATH_MAX]; + int error, ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_ATTRIB, path); + + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + error = fchmod(fd, 0600); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_ATTRIB, 0, NULL); + + error = fchown(fd, getuid(), getgid()); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_ATTRIB, 0, NULL); + + close_checked(fd); + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_close_nowrite); +ATF_TC_BODY(inotify_event_close_nowrite, tc) +{ + char file[PATH_MAX], file1[PATH_MAX], dir[PATH_MAX]; + int ifd, fd, wd1, wd2; + + ifd = inotify(IN_NONBLOCK); + + wd1 = watch_dir(ifd, IN_CLOSE_NOWRITE, dir); + wd2 = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, file); + + fd = open(dir, O_DIRECTORY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd1, IN_CLOSE_NOWRITE, IN_ISDIR, NULL); + + fd = open(file, O_RDONLY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd2, IN_CLOSE_NOWRITE, 0, NULL); + + snprintf(file1, sizeof(file1), "%s/file", dir); + fd = open(file1, O_RDONLY | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd1, IN_CLOSE_NOWRITE, 0, "file"); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_close_write); +ATF_TC_BODY(inotify_event_close_write, tc) +{ + char path[PATH_MAX]; + int ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, path); + + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_CLOSE_WRITE, 0, NULL); + + close_inotify(ifd); +} + +/* Verify that various operations in a directory generate IN_CREATE events. */ +ATF_TC_WITHOUT_HEAD(inotify_event_create); +ATF_TC_BODY(inotify_event_create, tc) +{ + struct sockaddr_un sun; + char path[PATH_MAX], path1[PATH_MAX], root[PATH_MAX]; + ssize_t n; + int error, ifd, ifd1, fd, s, wd, wd1; + char b; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_CREATE, root); + + /* Regular file. */ + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + /* + * Make sure we get an event triggered by the fd used to create the + * file. + */ + ifd1 = inotify(IN_NONBLOCK); + wd1 = inotify_add_watch(ifd1, root, IN_MODIFY); + b = 42; + n = write(fd, &b, sizeof(b)); + ATF_REQUIRE(n == sizeof(b)); + close_checked(fd); + consume_event(ifd, wd, IN_CREATE, 0, "file"); + consume_event(ifd1, wd1, IN_MODIFY, 0, "file"); + close_inotify(ifd1); + + /* Hard link. */ + snprintf(path1, sizeof(path1), "%s/link", root); + error = link(path, path1); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "link"); + + /* Directory. */ + snprintf(path, sizeof(path), "%s/dir", root); + error = mkdir(path, 0755); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, IN_ISDIR, "dir"); + + /* Symbolic link. */ + snprintf(path1, sizeof(path1), "%s/symlink", root); + error = symlink(path, path1); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "symlink"); + + /* FIFO. */ + snprintf(path, sizeof(path), "%s/fifo", root); + error = mkfifo(path, 0644); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_CREATE, 0, "fifo"); + + /* Binding a socket. */ + s = socket(AF_UNIX, SOCK_STREAM, 0); + memset(&sun, 0, sizeof(sun)); + sun.sun_family = AF_UNIX; + sun.sun_len = sizeof(sun); + snprintf(sun.sun_path, sizeof(sun.sun_path), "%s/socket", root); + error = bind(s, (struct sockaddr *)&sun, sizeof(sun)); + ATF_REQUIRE(error == 0); + close_checked(s); + consume_event(ifd, wd, IN_CREATE, 0, "socket"); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_delete); +ATF_TC_BODY(inotify_event_delete, tc) +{ + char root[PATH_MAX], path[PATH_MAX], file[PATH_MAX]; + int error, fd, ifd, wd, wd2; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_DELETE | IN_DELETE_SELF, root); + + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + error = unlink(path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE, 0, "file"); + close_checked(fd); + + /* + * Make sure that renaming over a file generates a delete event when and + * only when that file is watched. + */ + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + wd2 = inotify_add_watch(ifd, path, IN_DELETE | IN_DELETE_SELF); + ATF_REQUIRE(wd2 != -1); + snprintf(file, sizeof(file), "%s/file2", root); + fd = open(file, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + error = rename(file, path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd2, IN_DELETE_SELF, 0, NULL); + consume_event(ifd, wd2, 0, IN_IGNORED, NULL); + + error = unlink(path); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE, 0, "file"); + error = rmdir(root); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_move); +ATF_TC_BODY(inotify_event_move, tc) +{ + char dir1[PATH_MAX], dir2[PATH_MAX], path1[PATH_MAX], path2[PATH_MAX]; + char path3[PATH_MAX]; + int error, ifd, fd, wd1, wd2, wd3; + uint32_t cookie1, cookie2; + + ifd = inotify(IN_NONBLOCK); + + wd1 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir1); + wd2 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir2); + + snprintf(path1, sizeof(path1), "%s/file", dir1); + fd = open(path1, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + snprintf(path2, sizeof(path2), "%s/file2", dir2); + error = rename(path1, path2); + ATF_REQUIRE(error == 0); + cookie1 = consume_event_cookie(ifd, wd1, IN_MOVED_FROM, 0, "file"); + cookie2 = consume_event_cookie(ifd, wd2, IN_MOVED_TO, 0, "file2"); + ATF_REQUIRE_MSG(cookie1 == cookie2, + "expected cookie %u, got %u", cookie1, cookie2); + + snprintf(path2, sizeof(path2), "%s/dir", dir2); + error = rename(dir1, path2); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd1, IN_MOVE_SELF, IN_ISDIR, NULL); + consume_event(ifd, wd2, IN_MOVED_TO, IN_ISDIR, "dir"); + + wd3 = watch_file(ifd, IN_MOVE_SELF, path3); + error = rename(path3, "foo"); + ATF_REQUIRE(error == 0); + consume_event(ifd, wd3, IN_MOVE_SELF, 0, NULL); + + close_inotify(ifd); +} + +ATF_TC_WITHOUT_HEAD(inotify_event_open); +ATF_TC_BODY(inotify_event_open, tc) +{ + char root[PATH_MAX], path[PATH_MAX]; + int error, ifd, fd, wd; + + ifd = inotify(IN_NONBLOCK); + + wd = watch_dir(ifd, IN_OPEN, root); + + snprintf(path, sizeof(path), "%s/file", root); + fd = open(path, O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + fd = open(path, O_PATH); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "file"); + + fd = open(root, O_DIRECTORY); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + + snprintf(path, sizeof(path), "%s/fifo", root); + error = mkfifo(path, 0644); + ATF_REQUIRE(error == 0); + fd = open(path, O_RDWR); + ATF_REQUIRE(fd != -1); + close_checked(fd); + consume_event(ifd, wd, IN_OPEN, 0, "fifo"); + + close_inotify(ifd); +} + +ATF_TC_WITH_CLEANUP(inotify_event_unmount); +ATF_TC_HEAD(inotify_event_unmount, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(inotify_event_unmount, tc) +{ + int error, fd, ifd, wd; + + ifd = inotify(IN_NONBLOCK); + + error = mkdir("./root", 0755); + ATF_REQUIRE(error == 0); + + mount_tmpfs("./root"); + + error = mkdir("./root/dir", 0755); + ATF_REQUIRE(error == 0); + wd = inotify_add_watch(ifd, "./root/dir", IN_OPEN); + ATF_REQUIRE(wd >= 0); + + fd = open("./root/dir", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE(fd != -1); + consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL); + close_checked(fd); + + /* A regular unmount should fail, as inotify holds a vnode reference. */ + error = unmount("./root", 0); + ATF_REQUIRE_ERRNO(EBUSY, error == -1); + error = unmount("./root", MNT_FORCE); + ATF_REQUIRE_MSG(error == 0, + "unmounting ./root failed: %s", strerror(errno)); + + consume_event(ifd, wd, 0, IN_UNMOUNT, NULL); + consume_event(ifd, wd, 0, IN_IGNORED, NULL); + + close_inotify(ifd); +} +ATF_TC_CLEANUP(inotify_event_unmount, tc) +{ + (void)unmount("./root", MNT_FORCE); +} + +ATF_TP_ADD_TCS(tp) +{ + /* Tests for the inotify syscalls. */ + ATF_TP_ADD_TC(tp, inotify_capsicum); + ATF_TP_ADD_TC(tp, inotify_coalesce); + ATF_TP_ADD_TC(tp, inotify_mask_create); + ATF_TP_ADD_TC(tp, inotify_nullfs); + ATF_TP_ADD_TC(tp, inotify_queue_overflow); + /* Tests for the various inotify event types. */ + ATF_TP_ADD_TC(tp, inotify_event_access_file); + ATF_TP_ADD_TC(tp, inotify_event_access_dir); + ATF_TP_ADD_TC(tp, inotify_event_attrib); + ATF_TP_ADD_TC(tp, inotify_event_close_nowrite); + ATF_TP_ADD_TC(tp, inotify_event_close_write); + ATF_TP_ADD_TC(tp, inotify_event_create); + ATF_TP_ADD_TC(tp, inotify_event_delete); + ATF_TP_ADD_TC(tp, inotify_event_move); + ATF_TP_ADD_TC(tp, inotify_event_open); + ATF_TP_ADD_TC(tp, inotify_event_unmount); + return (atf_no_error()); +} diff --git a/tests/sys/kern/jail_lookup_root.c b/tests/sys/kern/jail_lookup_root.c new file mode 100644 index 000000000000..34e89f4aea2b --- /dev/null +++ b/tests/sys/kern/jail_lookup_root.c @@ -0,0 +1,133 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org> + */ + +#include <sys/param.h> +#include <sys/jail.h> +#include <sys/mount.h> +#include <sys/stat.h> + +#include <err.h> +#include <errno.h> +#include <fcntl.h> +#include <jail.h> +#include <mntopts.h> +#include <stdio.h> +#include <stdlib.h> + +#include <atf-c.h> + +static void +mkdir_checked(const char *dir, mode_t mode) +{ + int error; + + error = mkdir(dir, mode); + ATF_REQUIRE_MSG(error == 0 || errno == EEXIST, + "mkdir %s: %s", dir, strerror(errno)); +} + +static void __unused +mount_nullfs(const char *dir, const char *target) +{ + struct iovec *iov; + char errmsg[1024]; + int error, iovlen; + + iov = NULL; + iovlen = 0; + + build_iovec(&iov, &iovlen, __DECONST(char *, "fstype"), + __DECONST(char *, "nullfs"), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "fspath"), + __DECONST(char *, target), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "from"), + __DECONST(char *, dir), (size_t)-1); + build_iovec(&iov, &iovlen, __DECONST(char *, "errmsg"), + errmsg, sizeof(errmsg)); + + errmsg[0] = '\0'; + error = nmount(iov, iovlen, 0); + ATF_REQUIRE_MSG(error == 0, "nmount: %s", + errmsg[0] != '\0' ? errmsg : strerror(errno)); + + free_iovec(&iov, &iovlen); +} + +ATF_TC_WITH_CLEANUP(jail_root); +ATF_TC_HEAD(jail_root, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(jail_root, tc) +{ + int error, fd, jid; + + mkdir_checked("./root", 0755); + mkdir_checked("./root/a", 0755); + mkdir_checked("./root/b", 0755); + mkdir_checked("./root/a/c", 0755); + + jid = jail_setv(JAIL_CREATE | JAIL_ATTACH, + "name", "nullfs_jail_root_test", + "allow.mount", "true", + "allow.mount.nullfs", "true", + "enforce_statfs", "1", + "path", "./root", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid >= 0, "jail_setv: %s", jail_errmsg); + + mount_nullfs("/a", "/b"); + + error = chdir("/b/c"); + ATF_REQUIRE(error == 0); + + error = rename("/a/c", "/c"); + ATF_REQUIRE(error == 0); + + /* Descending to the jail root should be ok. */ + error = chdir(".."); + ATF_REQUIRE(error == 0); + + /* Going beyond the root will trigger an error. */ + error = chdir(".."); + ATF_REQUIRE_ERRNO(ENOENT, error != 0); + fd = open("..", O_RDONLY | O_DIRECTORY); + ATF_REQUIRE_ERRNO(ENOENT, fd < 0); +} +ATF_TC_CLEANUP(jail_root, tc) +{ + struct statfs fs; + fsid_t fsid; + int error, jid; + + error = statfs("./root/b", &fs); + if (error != 0) + err(1, "statfs ./b"); + fsid = fs.f_fsid; + error = statfs("./root", &fs); + if (error != 0) + err(1, "statfs ./root"); + if (fsid.val[0] != fs.f_fsid.val[0] || + fsid.val[1] != fs.f_fsid.val[1]) { + error = unmount("./root/b", 0); + if (error != 0) + err(1, "unmount ./root/b"); + } + + jid = jail_getid("nullfs_jail_root_test"); + if (jid >= 0) { + error = jail_remove(jid); + if (error != 0) + err(1, "jail_remove"); + } +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, jail_root); + return (atf_no_error()); +} diff --git a/tests/sys/kern/jailmeta.sh b/tests/sys/kern/jailmeta.sh new file mode 100644 index 000000000000..9a63f958231f --- /dev/null +++ b/tests/sys/kern/jailmeta.sh @@ -0,0 +1,588 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 SkunkWerks GmbH +# +# This software was developed by Igor Ostapenko <igoro@FreeBSD.org> +# under sponsorship from SkunkWerks GmbH. +# + +setup() +{ + # Check if we have enough buffer space for testing + if [ $(sysctl -n security.jail.meta_maxbufsize) -lt 128 ]; then + atf_skip "sysctl security.jail.meta_maxbufsize must be 128+ for testing." + fi +} + +atf_test_case "jail_create" "cleanup" +jail_create_head() +{ + atf_set descr 'Test that metadata can be set upon jail creation with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_create_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="a b c" env="C B A" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"C B A\n" \ + jls -jj env +} +jail_create_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jail_modify" "cleanup" +jail_modify_head() +{ + atf_set descr 'Test that metadata can be modified after jail creation with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_modify_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="a b c" env="CAB" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CAB\n" \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta="t1=A t2=B" env="CAB2" + + atf_check -s exit:0 -o inline:"t1=A t2=B\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CAB2\n" \ + jls -jj env +} +jail_modify_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jail_add" "cleanup" +jail_add_head() +{ + atf_set descr 'Test that metadata can be added to an existing jail with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_add_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist host.hostname=jail1 + + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta="$(jot 3 1 3)" env="$(jot 2 11 12)" + + atf_check -s exit:0 -o inline:"1\n2\n3\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"11\n12\n" \ + jls -jj env +} +jail_add_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jail_reset" "cleanup" +jail_reset_head() +{ + atf_set descr 'Test that metadata can be reset to an empty string with jail(8)' + atf_set require.user root + atf_set execenv jail +} +jail_reset_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="123" env="456" + + atf_check -s exit:0 -o inline:"123\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"456\n" \ + jls -jj env + + atf_check -s exit:0 \ + jail -m name=j meta= env= + + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -jj env +} +jail_reset_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "jls_libxo_json" "cleanup" +jls_libxo_json_head() +{ + atf_set descr 'Test that metadata can be read with jls(8) using libxo JSON' + atf_set require.user root + atf_set execenv jail +} +jls_libxo_json_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="a b c" env="1 2 3" + + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"name":"j","meta":"a b c"}]}}\n' \ + jls -jj --libxo json name meta + atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"env":"1 2 3"}]}}\n' \ + jls -jj --libxo json env +} +jls_libxo_json_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "flua_create" "cleanup" +flua_create_head() +{ + atf_set descr 'Test that metadata can be set upon jail creation with flua' + atf_set require.user root + atf_set execenv jail +} +flua_create_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v2", ["env"]="BAC", ["persist"]="true"}, jail.CREATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v2\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"meta"}); print(res["meta"])' + atf_check -s exit:0 -o inline:"BAC\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"env"}); print(res["env"])' +} +flua_create_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "flua_modify" "cleanup" +flua_modify_head() +{ + atf_set descr 'Test that metadata can be changed with flua after jail creation' + atf_set require.user root + atf_set execenv jail +} +flua_modify_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="ABC" env="123" + + atf_check -s exit:0 -o inline:"ABC\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"123\n" \ + jls -jj env + + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v", ["env"]="4"}, jail.UPDATE)' + + atf_check -s exit:0 -o inline:"t1 t2=v\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"4\n" \ + jls -jj env +} +flua_modify_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "env_readable_by_jail" "cleanup" +env_readable_by_jail_head() +{ + atf_set descr 'Test that a jail can read its own env parameter via sysctl(8)' + atf_set require.user root + atf_set execenv jail +} +env_readable_by_jail_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -jj + + atf_check -s exit:0 \ + jail -c name=j persist meta="a b c" env="CBA" + + atf_check -s exit:0 -o inline:"a b c\n" \ + jls -jj meta + atf_check -s exit:0 -o inline:"CBA\n" \ + jls -jj env + + atf_check -s exit:0 -o inline:"CBA\n" \ + jexec j sysctl -n security.jail.env +} +env_readable_by_jail_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "not_inheritable" "cleanup" +not_inheritable_head() +{ + atf_set descr 'Test that a jail does not inherit metadata from its parent jail' + atf_set require.user root + atf_set execenv jail +} +not_inheritable_body() +{ + setup + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j parent + + atf_check -s exit:0 \ + jail -c name=parent children.max=1 persist meta="abc" env="cba" + + jexec parent jail -c name=child persist + + atf_check -s exit:0 -o inline:"abc\n" \ + jls -j parent meta + atf_check -s exit:0 -o inline:'""\n' \ + jls -j parent.child meta + + atf_check -s exit:0 -o inline:"cba\n" \ + jexec parent sysctl -n security.jail.env + atf_check -s exit:0 -o inline:"\n" \ + jexec parent.child sysctl -n security.jail.env +} +not_inheritable_cleanup() +{ + jail -r parent.child + jail -r parent + return 0 +} + +atf_test_case "maxbufsize" "cleanup" +maxbufsize_head() +{ + atf_set descr 'Test that metadata buffer maximum size can be changed' + atf_set require.user root + atf_set is.exclusive true +} +maxbufsize_body() +{ + setup + + jn=jailmeta_maxbufsize + + atf_check -s not-exit:0 -e match:"not found" -o ignore \ + jls -j $jn + + # the size counts string length and the trailing \0 char + origmax=$(sysctl -n security.jail.meta_maxbufsize) + + # must be fine with current max + atf_check -s exit:0 \ + jail -c name=$jn persist meta="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn env="$(printf %$((origmax-1))s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c + + # should not allow exceeding current max + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn meta="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn env="$(printf %${origmax}s)" + + # should allow the same size with increased max + newmax=$((origmax + 1)) + sysctl security.jail.meta_maxbufsize=$newmax + atf_check -s exit:0 \ + jail -m name=$jn meta="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 \ + jail -m name=$jn env="$(printf %${origmax}s)" + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c + + # decrease back to the original max + sysctl security.jail.meta_maxbufsize=$origmax + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn meta="$(printf %${origmax}s)" + # + atf_check -s not-exit:0 -e match:"too large" \ + jail -m name=$jn env="$(printf %${origmax}s)" + + # the previously set long meta is still readable as is + # due to the soft limit remains higher than the hard limit + atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.meta)' + atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.env)' + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn meta | wc -c + # + atf_check -s exit:0 -o inline:"${origmax}\n" \ + jls -j $jn env | wc -c +} +maxbufsize_cleanup() +{ + jail -r jailmeta_maxbufsize + return 0 +} + +atf_test_case "keyvalue" "cleanup" +keyvalue_head() +{ + atf_set descr 'Test that metadata can be handled as a set of key=value\n strings using jail(8), jls(8), and flua' + atf_set require.user root + atf_set execenv jail +} +keyvalue_generic() +{ + local meta=$1 + + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta + + # Note: each sub-case depends on the results of the previous ones + + # Should be able to extract a key added manually + atf_check -sexit:0 jail -m name=j $meta="a=1" + atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta + atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a + atf_check -sexit:0 jail -m name=j $meta="$(printf 'a=2\nb=3')" + atf_check -sexit:0 -oinline:'a=2\nb=3\n' jls -jj $meta + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.b + + # Should provide nothing for a non-found key + atf_check -sexit:0 -oinline:'\n' jls -jj $meta.c + + # Should be able to lookup multiple keys at once + atf_check -sexit:0 -oinline:'3 2\n' jls -jj $meta.b $meta.a + + # Should be able to lookup keys and the whole buffer at once + atf_check -sexit:0 -oinline:'3 a=2\nb=3 2\n' jls -jj $meta.b $meta $meta.a + + # Should be able to lookup a key using libxo-based JSON output + s='{"__version": "2", "jail-information": {"jail": [{"'$meta'.b":"3"}]}}\n' + atf_check -s exit:0 -o inline:"$s" jls -jj --libxo json $meta.b + + # Should provide nothing for a non-found key using libxo-based JSON output + s='{"__version": "2", "jail-information": {"jail": [{}]}}\n' + atf_check -s exit:0 -o inline:"$s" jls -jj --libxo json $meta.c $meta.d + + # Should be able to lookup a key using flua + atf_check -s exit:0 -o inline:"2\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"'$meta'.a"}); print(res["'$meta'.a"])' + + # Should provide nil for a non-found key using flua + atf_check -s exit:0 -o inline:"true\n" \ + /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"'$meta'.meta"}); print(res["'$meta'.meta"] == nil)' + + # Should allow resetting a buffer + atf_check -sexit:0 jail -m name=j $meta= + atf_check -sexit:0 -oinline:' "" \n' jls -jj $meta.c $meta $meta.a + + # Should allow adding a new key + atf_check -sexit:0 jail -m name=j $meta.a=1 + atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta + + # Should allow adding multiple new keys at once + atf_check -sexit:0 jail -m name=j $meta.c=3 $meta.b=2 + atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.c + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b=2\nc=3\na=1\n' jls -jj $meta + + # Should replace existing keys + atf_check -sexit:0 jail -m name=j $meta.a=A $meta.c=C + atf_check -sexit:0 -oinline:'A\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c + atf_check -sexit:0 -oinline:'c=C\na=A\nb=2\n' jls -jj $meta + + # Should treat empty value correctly + atf_check -sexit:0 jail -m name=j $meta.a= + atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'a=\nc=C\nb=2\n' jls -jj $meta + + # Should treat NULL value as a key removal + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b + atf_check -sexit:0 jail -m name=j $meta.b + atf_check -sexit:0 -oinline:'\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'a=\nc=C\n' jls -jj $meta + + # Should allow changing the whole buffer and per key at once (order matters) + atf_check -sexit:0 jail -m name=j $meta.a=1 $meta=ttt $meta.b=2 + atf_check -sexit:0 -oinline:'\n' jls -jj $meta.a + atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b=2\nttt\n' jls -jj $meta + + # Should treat only the first equal sign as syntax + atf_check -sexit:0 jail -m name=j $meta.b== + atf_check -sexit:0 -oinline:'=\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'b==\nttt\n' jls -jj $meta + + # Should allow adding or modifying keys using flua + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.b'"]="ttt", ["'$meta'.c"]="C"}, jail.UPDATE)' + atf_check -sexit:0 -oinline:'ttt\n' jls -jj $meta.b + atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c + + # Should allow key removal using flua + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.c'"] = {}}, jail.UPDATE)' + atf_check -sexit:0 -oinline:'\n' jls -jj $meta.c + atf_check -s exit:0 \ + /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.b'"] = false}, jail.UPDATE)' + atf_check -sexit:0 -oinline:'\n' jls -jj $meta.b + + # Should respectively support "jls -s" for a missing key + atf_check -sexit:0 -oinline:''$meta'.missing\n' jls -jj -s $meta.missing +} +keyvalue_body() +{ + setup + + atf_check -s exit:0 \ + jail -c name=j persist meta env + + keyvalue_generic "meta" + keyvalue_generic "env" +} +keyvalue_cleanup() +{ + jail -r j + return 0 +} + +atf_test_case "keyvalue_contention" "cleanup" +keyvalue_contention_head() +{ + atf_set descr 'Try to stress metadata read/write mechanism with some contention' + atf_set require.user root + atf_set execenv jail + atf_set timeout 30 +} +keyvalue_stresser() +{ + local jailname=$1 + local modifier=$2 + + while true + do + jail -m name=$jailname $modifier + done +} +keyvalue_contention_body() +{ + setup + + atf_check -s exit:0 jail -c name=j persist meta env + + keyvalue_stresser "j" "meta.a=1" & + apid=$! + keyvalue_stresser "j" "meta.b=2" & + bpid=$! + keyvalue_stresser "j" "env.c=3" & + cpid=$! + keyvalue_stresser "j" "env.d=4" & + dpid=$! + + for it in $(jot 8) + do + jail -m name=j meta='meta=META' env='env=ENV' + sleep 1 + atf_check -sexit:0 -oinline:'1\n' jls -jj meta.a + atf_check -sexit:0 -oinline:'2\n' jls -jj meta.b + atf_check -sexit:0 -oinline:'3\n' jls -jj env.c + atf_check -sexit:0 -oinline:'4\n' jls -jj env.d + atf_check -sexit:0 -oinline:'META\n' jls -jj meta.meta + atf_check -sexit:0 -oinline:'ENV\n' jls -jj env.env + done + + # TODO: Think of adding a stresser on the kernel side which does + # osd_set() w/o allprison lock. It could test the compare + # and swap mechanism in jm_osd_method_set(). + + kill -9 $apid $bpid $cpid $dpid +} +keyvalue_contention_cleanup() +{ + jail -r j + return 0 +} + +atf_init_test_cases() +{ + atf_add_test_case "jail_create" + atf_add_test_case "jail_modify" + atf_add_test_case "jail_add" + atf_add_test_case "jail_reset" + + atf_add_test_case "jls_libxo_json" + + atf_add_test_case "flua_create" + atf_add_test_case "flua_modify" + + atf_add_test_case "env_readable_by_jail" + atf_add_test_case "not_inheritable" + + atf_add_test_case "maxbufsize" + + atf_add_test_case "keyvalue" + atf_add_test_case "keyvalue_contention" +} diff --git a/tests/sys/kern/kern_copyin.c b/tests/sys/kern/kern_copyin.c index 6ad195d5d055..e677081a5c70 100644 --- a/tests/sys/kern/kern_copyin.c +++ b/tests/sys/kern/kern_copyin.c @@ -1,6 +1,5 @@ /*- * Copyright (c) 2015, 2020 The FreeBSD Foundation - * All rights reserved. * * This software was developed by Konstantin Belousov <kib@FreeBSD.org> * under sponsorship from the FreeBSD Foundation. diff --git a/tests/sys/kern/ktls_test.c b/tests/sys/kern/ktls_test.c index f57ae74112a2..72497196b945 100644 --- a/tests/sys/kern/ktls_test.c +++ b/tests/sys/kern/ktls_test.c @@ -2812,7 +2812,7 @@ ATF_TC_BODY(ktls_listening_socket, tc) TLS_MINOR_VER_THREE, (uint64_t)random(), &en); ATF_REQUIRE_ERRNO(ENOTCONN, setsockopt(s, IPPROTO_TCP, TCP_TXTLS_ENABLE, &en, sizeof(en)) != 0); - ATF_REQUIRE_ERRNO(EINVAL, + ATF_REQUIRE_ERRNO(ENOTCONN, setsockopt(s, IPPROTO_TCP, TCP_RXTLS_ENABLE, &en, sizeof(en)) != 0); ATF_REQUIRE(close(s) == 0); } diff --git a/tests/sys/kern/ktrace_test.c b/tests/sys/kern/ktrace_test.c index 21868441c687..785c78bedaba 100644 --- a/tests/sys/kern/ktrace_test.c +++ b/tests/sys/kern/ktrace_test.c @@ -33,6 +33,7 @@ #include <sys/ktrace.h> #include <sys/mman.h> #include <sys/socket.h> +#include <sys/syscall.h> #include <sys/sysent.h> #include <sys/time.h> #include <sys/uio.h> @@ -86,6 +87,16 @@ child_fail_require(const char *file, int line, const char *fmt, ...) _exit(32); } +static void * +xmalloc(size_t size) +{ + void *p; + + p = malloc(size); + ATF_REQUIRE(p != NULL); + return (p); +} + /* * Determine sysdecode ABI based on proc's ABI in sv_flags. */ @@ -105,36 +116,47 @@ syscallabi(u_int sv_flags) return (SYSDECODE_ABI_UNKNOWN); } -/* - * Start tracing capability violations and notify child that it can execute. - * Return @numv capability violations from child in @v. - */ -static void -cap_trace_child(int cpid, struct ktr_cap_fail *v, int numv) +static int +trace_child(int cpid, int facility, int status) { - struct ktr_header header; - int error, fd, i; + int error, fd; ATF_REQUIRE((fd = open("ktrace.out", - O_RDONLY | O_CREAT | O_TRUNC)) != -1); - ATF_REQUIRE(ktrace("ktrace.out", KTROP_SET, - KTRFAC_CAPFAIL, cpid) != -1); + O_RDONLY | O_CREAT | O_TRUNC, 0600)) != -1); + ATF_REQUIRE_MSG(ktrace("ktrace.out", KTROP_SET, facility, cpid) != -1, + "ktrace failed: %s", strerror(errno)); /* Notify child that we've starting tracing. */ ATF_REQUIRE(kill(cpid, SIGUSR1) != -1); /* Wait for child to raise violation and exit. */ ATF_REQUIRE(waitpid(cpid, &error, 0) != -1); ATF_REQUIRE(WIFEXITED(error)); - ATF_REQUIRE_EQ(WEXITSTATUS(error), 0); + ATF_REQUIRE_EQ(WEXITSTATUS(error), status); + return (fd); +} + +/* + * Start tracing capability violations and notify child that it can execute. + * Return @numv capability violations from child in @v. + */ +static void +cap_trace_child(pid_t cpid, struct ktr_cap_fail *v, int numv) +{ + struct ktr_header header; + ssize_t n; + int fd; + + fd = trace_child(cpid, KTRFAC_CAPFAIL, 0); + /* Read ktrace header and ensure violation occurred. */ - for (i = 0; i < numv; ++i) { - ATF_REQUIRE((error = read(fd, &header, sizeof(header))) != -1); - ATF_REQUIRE_EQ(error, sizeof(header)); + for (int i = 0; i < numv; ++i) { + ATF_REQUIRE((n = read(fd, &header, sizeof(header))) != -1); + ATF_REQUIRE_EQ(n, sizeof(header)); ATF_REQUIRE_EQ(header.ktr_len, sizeof(*v)); ATF_REQUIRE_EQ(header.ktr_pid, cpid); /* Read the capability violation. */ - ATF_REQUIRE((error = read(fd, v + i, + ATF_REQUIRE((n = read(fd, v + i, sizeof(*v))) != -1); - ATF_REQUIRE_EQ(error, sizeof(*v)); + ATF_REQUIRE_EQ(n, sizeof(*v)); } ATF_REQUIRE(close(fd) != -1); } @@ -301,7 +323,11 @@ ATF_TC_BODY(ktrace__cap_signal, tc) * Test if opening a socket with a restricted protocol is reported * as a protocol violation. */ -ATF_TC_WITHOUT_HEAD(ktrace__cap_proto); +ATF_TC(ktrace__cap_proto); +ATF_TC_HEAD(ktrace__cap_proto, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} ATF_TC_BODY(ktrace__cap_proto, tc) { struct ktr_cap_fail violation; @@ -352,12 +378,11 @@ ATF_TC_BODY(ktrace__cap_sockaddr, tc) ATF_REQUIRE(sigaddset(&set, SIGUSR1) != -1); ATF_REQUIRE(sigprocmask(SIG_BLOCK, &set, NULL) != -1); - CHILD_REQUIRE((sfd = socket(AF_INET, SOCK_DGRAM, - IPPROTO_UDP)) != -1); + ATF_REQUIRE((sfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1); addr.sin_family = AF_INET; addr.sin_port = htons(5000); - addr.sin_addr.s_addr = INADDR_ANY; - CHILD_REQUIRE(bind(sfd, (const struct sockaddr *)&addr, + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + ATF_REQUIRE(bind(sfd, (const struct sockaddr *)&addr, sizeof(addr)) != -1); ATF_REQUIRE((pid = fork()) != -1); @@ -383,7 +408,7 @@ ATF_TC_BODY(ktrace__cap_sockaddr, tc) saddr = (struct sockaddr_in *)&violation.cap_data.cap_sockaddr; ATF_REQUIRE_EQ(saddr->sin_family, AF_INET); ATF_REQUIRE_EQ(saddr->sin_port, htons(5000)); - ATF_REQUIRE_EQ(saddr->sin_addr.s_addr, INADDR_ANY); + ATF_REQUIRE_EQ(saddr->sin_addr.s_addr, htonl(INADDR_LOOPBACK)); close(sfd); } @@ -507,6 +532,78 @@ ATF_TC_BODY(ktrace__cap_shm_open, tc) ATF_REQUIRE_STREQ(violation.cap_data.cap_path, "/ktrace_shm"); } +/* + * Make sure that ktrace is disabled upon exec of a setuid binary. + */ +ATF_TC(ktrace__setuid_exec); +ATF_TC_HEAD(ktrace__setuid_exec, tc) +{ + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(ktrace__setuid_exec, tc) +{ + struct ktr_header header; + struct ktr_syscall *syscall; + sigset_t set = { }; + off_t off, off1; + ssize_t n; + pid_t pid; + int error, fd; + + /* Block SIGUSR1 so child does not terminate. */ + ATF_REQUIRE(sigaddset(&set, SIGUSR1) != -1); + ATF_REQUIRE(sigprocmask(SIG_BLOCK, &set, NULL) != -1); + + ATF_REQUIRE((pid = fork()) != -1); + if (pid == 0) { + /* Wait until ktrace has started. */ + CHILD_REQUIRE(sigwait(&set, &error) != -1); + CHILD_REQUIRE_EQ(error, SIGUSR1); + + execve("/usr/bin/su", (char *[]){ "su", "whoami", NULL }, NULL); + _exit(0); + } + + fd = trace_child(pid, KTRFAC_SYSCALL, 1); + + n = read(fd, &header, sizeof(header)); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ((size_t)n, sizeof(header)); + ATF_REQUIRE_EQ(header.ktr_pid, pid); + ATF_REQUIRE(header.ktr_len >= (int)sizeof(*syscall)); + + syscall = xmalloc(header.ktr_len); + n = read(fd, syscall, header.ktr_len); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ(n, header.ktr_len); + if (syscall->ktr_code == SYS_sigwait) { + free(syscall); + + /* Skip the sigwait() syscall. */ + n = read(fd, &header, sizeof(header)); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ((size_t)n, sizeof(header)); + ATF_REQUIRE_EQ(header.ktr_pid, pid); + ATF_REQUIRE(header.ktr_len >= (int)sizeof(*syscall)); + + syscall = xmalloc(header.ktr_len); + n = read(fd, syscall, header.ktr_len); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ(n, header.ktr_len); + } + ATF_REQUIRE_EQ(syscall->ktr_code, SYS_execve); + free(syscall); + + /* su is setuid root, so this should have been the last entry. */ + off = lseek(fd, 0, SEEK_CUR); + ATF_REQUIRE(off != -1); + off1 = lseek(fd, 0, SEEK_END); + ATF_REQUIRE(off1 != -1); + ATF_REQUIRE_EQ(off, off1); + + ATF_REQUIRE(close(fd) == 0); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, ktrace__cap_not_capable); @@ -518,5 +615,6 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, ktrace__cap_namei); ATF_TP_ADD_TC(tp, ktrace__cap_cpuset); ATF_TP_ADD_TC(tp, ktrace__cap_shm_open); + ATF_TP_ADD_TC(tp, ktrace__setuid_exec); return (atf_no_error()); } diff --git a/tests/sys/kern/logsigexit_test.sh b/tests/sys/kern/logsigexit_test.sh new file mode 100644 index 000000000000..f0db02613533 --- /dev/null +++ b/tests/sys/kern/logsigexit_test.sh @@ -0,0 +1,37 @@ +# +# Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +# +# SPDX-License-Identifier: BSD-2-Clause +# + +atf_test_case basic +basic_body() +{ + + if ! dmesg >/dev/null 2>&1; then + atf_skip "No dmesg(8) access" + fi + + # SIGABRT carefully chosen to avoid issues when run under Kyua. No + # matter the value of the global kern.logsigexit, these should force + # the messages as appropriate and we'll all be happy. + proccontrol -m logsigexit -s enable \ + sh -c 'echo $$ > enabled.out; kill -ABRT $$' + proccontrol -m logsigexit -s disable \ + sh -c 'echo $$ > disabled.out; kill -ABRT $$' + + atf_check test -s enabled.out + atf_check test -s disabled.out + + read enpid < enabled.out + read dispid < disabled.out + + atf_check -o save:dmesg.out dmesg + atf_check grep -Eq "$enpid.+exited on signal" dmesg.out + atf_check -s not-exit:0 grep -Eq "$dispid.+exited on signal" dmesg.out +} + +atf_init_test_cases() +{ + atf_add_test_case basic +} diff --git a/tests/sys/kern/pipe/Makefile b/tests/sys/kern/pipe/Makefile index e9bc0b048c5b..e8fa05ee8e0e 100644 --- a/tests/sys/kern/pipe/Makefile +++ b/tests/sys/kern/pipe/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/kern/pipe PLAIN_TESTS_C+= big_pipe_test diff --git a/tests/sys/kern/prace.c b/tests/sys/kern/prace.c new file mode 100644 index 000000000000..e6aa09ec2180 --- /dev/null +++ b/tests/sys/kern/prace.c @@ -0,0 +1,144 @@ +/*- + * Copyright (c) 2024 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + * + * These tests demonstrate a bug in ppoll() and pselect() where a blocked + * signal can fire after the timer runs out but before the signal mask is + * restored. To do this, we fork a child process which installs a SIGINT + * handler and repeatedly calls either ppoll() or pselect() with a 1 ms + * timeout, while the parent repeatedly sends SIGINT to the child at + * intervals that start out at 1100 us and gradually decrease to 900 us. + * Each SIGINT resynchronizes parent and child, and sooner or later the + * parent hits the sweet spot and the SIGINT arrives at just the right + * time to demonstrate the bug. + */ + +#include <sys/select.h> +#include <sys/wait.h> + +#include <err.h> +#include <errno.h> +#include <poll.h> +#include <signal.h> +#include <stdbool.h> +#include <stdlib.h> +#include <unistd.h> + +#include <atf-c.h> + +static volatile sig_atomic_t caught[NSIG]; + +static void +handler(int signo) +{ + caught[signo]++; +} + +static void +child(int rd, bool poll) +{ + struct timespec timeout = { .tv_nsec = 1000000 }; + sigset_t set0, set1; + int ret; + + /* empty mask for ppoll() / pselect() */ + sigemptyset(&set0); + + /* block SIGINT, then install a handler for it */ + sigemptyset(&set1); + sigaddset(&set1, SIGINT); + sigprocmask(SIG_BLOCK, &set1, NULL); + signal(SIGINT, handler); + + /* signal parent that we are ready */ + close(rd); + for (;;) { + /* sleep for 1 ms with signals unblocked */ + ret = poll ? ppoll(NULL, 0, &timeout, &set0) : + pselect(0, NULL, NULL, NULL, &timeout, &set0); + /* + * At this point, either ret == 0 (timer ran out) errno == + * EINTR (a signal was received). Any other outcome is + * abnormal. + */ + if (ret != 0 && errno != EINTR) + err(1, "p%s()", poll ? "poll" : "select"); + /* if ret == 0, we should not have caught any signals */ + if (ret == 0 && caught[SIGINT]) { + /* + * We successfully demonstrated the race. Restore + * the default action and re-raise SIGINT. + */ + signal(SIGINT, SIG_DFL); + raise(SIGINT); + /* Not reached */ + } + /* reset for next attempt */ + caught[SIGINT] = 0; + } + /* Not reached */ +} + +static void +prace(bool poll) +{ + int pd[2], status; + pid_t pid; + + /* fork child process */ + if (pipe(pd) != 0) + err(1, "pipe()"); + if ((pid = fork()) < 0) + err(1, "fork()"); + if (pid == 0) { + close(pd[0]); + child(pd[1], poll); + /* Not reached */ + } + close(pd[1]); + + /* wait for child to signal readiness */ + (void)read(pd[0], &pd[0], sizeof(pd[0])); + close(pd[0]); + + /* repeatedly attempt to signal at just the right moment */ + for (useconds_t timeout = 1100; timeout > 900; timeout--) { + usleep(timeout); + if (kill(pid, SIGINT) != 0) { + if (errno != ENOENT) + err(1, "kill()"); + /* ENOENT means the child has terminated */ + break; + } + } + + /* we're done, kill the child for sure */ + (void)kill(pid, SIGKILL); + if (waitpid(pid, &status, 0) < 0) + err(1, "waitpid()"); + + /* assert that the child died of SIGKILL */ + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_MSG(WTERMSIG(status) == SIGKILL, + "child caught SIG%s", sys_signame[WTERMSIG(status)]); +} + +ATF_TC_WITHOUT_HEAD(ppoll_race); +ATF_TC_BODY(ppoll_race, tc) +{ + prace(true); +} + +ATF_TC_WITHOUT_HEAD(pselect_race); +ATF_TC_BODY(pselect_race, tc) +{ + prace(false); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, ppoll_race); + ATF_TP_ADD_TC(tp, pselect_race); + return (atf_no_error()); +} diff --git a/tests/sys/kern/ptrace_test.c b/tests/sys/kern/ptrace_test.c index 675d90e144ae..d36dfe951e20 100644 --- a/tests/sys/kern/ptrace_test.c +++ b/tests/sys/kern/ptrace_test.c @@ -28,13 +28,13 @@ #include <sys/elf.h> #include <sys/event.h> #include <sys/file.h> +#include <sys/mman.h> #include <sys/time.h> #include <sys/procctl.h> #include <sys/procdesc.h> #include <sys/ptrace.h> #include <sys/procfs.h> #include <sys/queue.h> -#include <sys/runq.h> #include <sys/syscall.h> #include <sys/sysctl.h> #include <sys/user.h> @@ -2027,7 +2027,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_signal, tc) sched_get_priority_min(SCHED_FIFO)) / 2; CHILD_REQUIRE(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sched_param) == 0); - sched_param.sched_priority -= RQ_PPQ; + sched_param.sched_priority -= 1; CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO, &sched_param) == 0); @@ -2130,7 +2130,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_stop, tc) sched_get_priority_min(SCHED_FIFO)) / 2; CHILD_REQUIRE(pthread_setschedparam(pthread_self(), SCHED_FIFO, &sched_param) == 0); - sched_param.sched_priority -= RQ_PPQ; + sched_param.sched_priority -= 1; CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO, &sched_param) == 0); @@ -4161,6 +4161,53 @@ ATF_TC_BODY(ptrace__syscall_args, tc) } /* + * Check that syscall info is available whenever kernel has valid td_sa. + * Assumes that libc nanosleep(2) is the plain syscall wrapper. + */ +ATF_TC_WITHOUT_HEAD(ptrace__syscall_args_anywhere); +ATF_TC_BODY(ptrace__syscall_args_anywhere, tc) +{ + struct timespec rqt; + struct ptrace_lwpinfo lwpi; + register_t args[8]; + pid_t debuggee, wpid; + int error, status; + + debuggee = fork(); + ATF_REQUIRE(debuggee >= 0); + if (debuggee == 0) { + rqt.tv_sec = 100000; + rqt.tv_nsec = 0; + for (;;) + nanosleep(&rqt, NULL); + _exit(0); + } + + /* Give the debuggee some time to go to sleep. */ + sleep(2); + error = ptrace(PT_ATTACH, debuggee, 0, 0); + ATF_REQUIRE(error == 0); + wpid = waitpid(debuggee, &status, 0); + REQUIRE_EQ(wpid, debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); + + error = ptrace(PT_LWPINFO, debuggee, (caddr_t)&lwpi, sizeof(lwpi)); + ATF_REQUIRE(error == 0); + ATF_REQUIRE(lwpi.pl_syscall_code == SYS_nanosleep); + ATF_REQUIRE(lwpi.pl_syscall_narg == 2); + error = ptrace(PT_GET_SC_ARGS, debuggee, (caddr_t)&args[0], + lwpi.pl_syscall_narg * sizeof(register_t)); + ATF_REQUIRE(error == 0); + ATF_REQUIRE(args[0] == (register_t)&rqt); + ATF_REQUIRE(args[1] == 0); + + error = ptrace(PT_DETACH, debuggee, 0, 0); + ATF_REQUIRE(error == 0); + kill(SIGKILL, debuggee); +} + +/* * Verify that when the process is traced that it isn't reparent * to the init process when we close all process descriptors. */ @@ -4331,7 +4378,10 @@ ATF_TC_BODY(ptrace__PT_SC_REMOTE_getpid, tc) exit(0); } - attach_child(fpid); + wpid = waitpid(fpid, &status, 0); + REQUIRE_EQ(wpid, fpid); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); pscr.pscr_syscall = SYS_getpid; pscr.pscr_nargs = 0; @@ -4414,6 +4464,132 @@ ATF_TC_BODY(ptrace__reap_kill_stopped, tc) REQUIRE_EQ(-1, prk.rk_fpid); } +struct child_res { + struct timespec sleep_time; + int nanosleep_res; + int nanosleep_errno; +}; + +static const long nsec = 1000000000L; +static const struct timespec ten_sec = { + .tv_sec = 10, + .tv_nsec = 0, +}; +static const struct timespec twelve_sec = { + .tv_sec = 12, + .tv_nsec = 0, +}; + +ATF_TC_WITHOUT_HEAD(ptrace__PT_ATTACH_no_EINTR); +ATF_TC_BODY(ptrace__PT_ATTACH_no_EINTR, tc) +{ + struct child_res *shm; + struct timespec rqt, now, wake; + pid_t debuggee; + int status; + + shm = mmap(NULL, sizeof(*shm), PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANON, -1, 0); + ATF_REQUIRE(shm != MAP_FAILED); + + ATF_REQUIRE((debuggee = fork()) != -1); + if (debuggee == 0) { + rqt.tv_sec = 10; + rqt.tv_nsec = 0; + clock_gettime(CLOCK_MONOTONIC_PRECISE, &now); + errno = 0; + shm->nanosleep_res = nanosleep(&rqt, NULL); + shm->nanosleep_errno = errno; + clock_gettime(CLOCK_MONOTONIC_PRECISE, &wake); + timespecsub(&wake, &now, &shm->sleep_time); + _exit(0); + } + + /* Give the debuggee some time to go to sleep. */ + sleep(2); + REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); + + REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + + ATF_REQUIRE(shm->nanosleep_res == 0); + ATF_REQUIRE(shm->nanosleep_errno == 0); + ATF_REQUIRE(timespeccmp(&shm->sleep_time, &ten_sec, >=)); + ATF_REQUIRE(timespeccmp(&shm->sleep_time, &twelve_sec, <=)); +} + +ATF_TC_WITHOUT_HEAD(ptrace__PT_DETACH_continued); +ATF_TC_BODY(ptrace__PT_DETACH_continued, tc) +{ + char buf[256]; + pid_t debuggee, debugger; + int dpipe[2] = {-1, -1}, status; + + /* Setup the debuggee's pipe, which we'll use to let it terminate. */ + ATF_REQUIRE(pipe(dpipe) == 0); + ATF_REQUIRE((debuggee = fork()) != -1); + + if (debuggee == 0) { + ssize_t readsz; + + /* + * The debuggee will just absorb everything until the parent + * closes it. In the process, we expect it to get SIGSTOP'd, + * then ptrace(2)d and finally, it should resume after we detach + * and the parent will be notified. + */ + close(dpipe[1]); + while ((readsz = read(dpipe[0], buf, sizeof(buf))) != 0) { + if (readsz > 0 || errno == EINTR) + continue; + _exit(1); + } + + _exit(0); + } + + close(dpipe[0]); + + ATF_REQUIRE(kill(debuggee, SIGSTOP) == 0); + REQUIRE_EQ(waitpid(debuggee, &status, WUNTRACED), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + + /* Child is stopped, enter the debugger to attach/detach. */ + ATF_REQUIRE((debugger = fork()) != -1); + if (debugger == 0) { + REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0); + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFSTOPPED(status)); + REQUIRE_EQ(WSTOPSIG(status), SIGSTOP); + + REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0); + _exit(0); + } + + REQUIRE_EQ(waitpid(debugger, &status, 0), debugger); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + + REQUIRE_EQ(waitpid(debuggee, &status, WCONTINUED), debuggee); + ATF_REQUIRE(WIFCONTINUED(status)); + + /* + * Closing the pipe will trigger the debuggee to exit now that the + * child has resumed following detach. + */ + close(dpipe[1]); + + REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee); + ATF_REQUIRE(WIFEXITED(status)); + REQUIRE_EQ(WEXITSTATUS(status), 0); + +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_trace_me); @@ -4476,11 +4652,14 @@ ATF_TP_ADD_TCS(tp) #endif ATF_TP_ADD_TC(tp, ptrace__PT_LWPINFO_stale_siginfo); ATF_TP_ADD_TC(tp, ptrace__syscall_args); + ATF_TP_ADD_TC(tp, ptrace__syscall_args_anywhere); ATF_TP_ADD_TC(tp, ptrace__proc_reparent); ATF_TP_ADD_TC(tp, ptrace__procdesc_wait_child); ATF_TP_ADD_TC(tp, ptrace__procdesc_reparent_wait_child); ATF_TP_ADD_TC(tp, ptrace__PT_SC_REMOTE_getpid); ATF_TP_ADD_TC(tp, ptrace__reap_kill_stopped); + ATF_TP_ADD_TC(tp, ptrace__PT_ATTACH_no_EINTR); + ATF_TP_ADD_TC(tp, ptrace__PT_DETACH_continued); return (atf_no_error()); } diff --git a/tests/sys/kern/sched_affinity.c b/tests/sys/kern/sched_affinity.c index cbba226b3fa2..9af6076c1649 100644 --- a/tests/sys/kern/sched_affinity.c +++ b/tests/sys/kern/sched_affinity.c @@ -67,13 +67,16 @@ ATF_TC_BODY(test_setinvalidcpu, tc) { size_t cpusetsize; cpuset_t *set; + int cpu; + + cpu = maxcpuid > 1 ? maxcpuid - 1 : 0; cpusetsize = CPU_ALLOC_SIZE(maxcpuid + 1); set = CPU_ALLOC(maxcpuid + 1); ATF_REQUIRE(set != NULL); CPU_ZERO_S(cpusetsize, set); CPU_SET_S(maxcpuid + 1, cpusetsize, set); - CPU_SET_S(maxcpuid - 1, cpusetsize, set); + CPU_SET_S(cpu, cpusetsize, set); ATF_REQUIRE(sched_setaffinity(0, cpusetsize, set) == 0); CPU_FREE(set); @@ -82,7 +85,7 @@ ATF_TC_BODY(test_setinvalidcpu, tc) ATF_REQUIRE(set != NULL); CPU_ZERO_S(cpusetsize, set); CPU_SET_S(maxcpuid + 1, cpusetsize, set); - CPU_SET_S(maxcpuid - 1, cpusetsize, set); + CPU_SET_S(cpu, cpusetsize, set); ATF_REQUIRE(cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, cpusetsize, set) == -1); ATF_REQUIRE_EQ(errno, EINVAL); diff --git a/tests/sys/kern/sendfile_helper.c b/tests/sys/kern/sendfile_helper.c index 703b04fdea6c..6365531e312c 100644 --- a/tests/sys/kern/sendfile_helper.c +++ b/tests/sys/kern/sendfile_helper.c @@ -84,7 +84,7 @@ tcp_socketpair(int *sv) if (fcntl(sv[0], F_SETFL, flags) == -1) err(1, "fcntl +O_NONBLOCK"); - if (connect(sv[0], (void *)&sin, sizeof(sin)) != -1 || + if (connect(sv[0], (void *)&sin, sizeof(sin)) == -1 && errno != EINPROGRESS) err(1, "connect cs"); diff --git a/tests/sys/kern/socket_splice.c b/tests/sys/kern/socket_splice.c new file mode 100644 index 000000000000..dfd4cb4f5957 --- /dev/null +++ b/tests/sys/kern/socket_splice.c @@ -0,0 +1,979 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 Stormshield + */ + +#include <sys/capsicum.h> +#include <sys/event.h> +#include <sys/filio.h> +#include <sys/socket.h> +#include <sys/wait.h> + +#include <netinet/in.h> +#include <netinet/tcp.h> + +#include <errno.h> +#include <poll.h> +#include <pthread.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include <atf-c.h> + +static void +checked_close(int fd) +{ + int error; + + error = close(fd); + ATF_REQUIRE_MSG(error == 0, "close failed: %s", strerror(errno)); +} + +static int +fionread(int fd) +{ + int data, error; + + data = 0; + error = ioctl(fd, FIONREAD, &data); + ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s", strerror(errno)); + ATF_REQUIRE(data >= 0); + return (data); +} + +static void +noblocking(int fd) +{ + int flags, error; + + flags = fcntl(fd, F_GETFL); + ATF_REQUIRE_MSG(flags != -1, "fcntl failed: %s", strerror(errno)); + flags |= O_NONBLOCK; + error = fcntl(fd, F_SETFL, flags); + ATF_REQUIRE_MSG(error == 0, "fcntl failed: %s", strerror(errno)); +} + +/* + * Create a pair of connected TCP sockets, returned via the "out" array. + */ +static void +tcp_socketpair(int out[2], int domain) +{ + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + struct sockaddr *sinp; + int error, sd[2]; + + sd[0] = socket(domain, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd[0] >= 0, "socket failed: %s", strerror(errno)); + sd[1] = socket(domain, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd[1] >= 0, "socket failed: %s", strerror(errno)); + + error = setsockopt(sd[0], IPPROTO_TCP, TCP_NODELAY, &(int){ 1 }, + sizeof(int)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + error = setsockopt(sd[1], IPPROTO_TCP, TCP_NODELAY, &(int){ 1 }, + sizeof(int)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + if (domain == PF_INET) { + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + sin.sin_port = htons(0); + sinp = (struct sockaddr *)&sin; + } else { + ATF_REQUIRE(domain == PF_INET6); + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_addr = in6addr_loopback; + sin6.sin6_port = htons(0); + sinp = (struct sockaddr *)&sin6; + } + + error = bind(sd[0], sinp, sinp->sa_len); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(sd[0], 1); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + error = getsockname(sd[0], sinp, &(socklen_t){ sinp->sa_len }); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + error = connect(sd[1], sinp, sinp->sa_len); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno)); + out[0] = accept(sd[0], NULL, NULL); + ATF_REQUIRE_MSG(out[0] >= 0, "accept failed: %s", strerror(errno)); + checked_close(sd[0]); + out[1] = sd[1]; +} + +static void +tcp4_socketpair(int out[2]) +{ + tcp_socketpair(out, PF_INET); +} + +static void +tcp6_socketpair(int out[2]) +{ + tcp_socketpair(out, PF_INET6); +} + +static off_t +nspliced(int sd) +{ + off_t n; + socklen_t len; + int error; + + len = sizeof(n); + error = getsockopt(sd, SOL_SOCKET, SO_SPLICE, &n, &len); + ATF_REQUIRE_MSG(error == 0, "getsockopt failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(len == sizeof(n), "unexpected length: %d", len); + return (n); +} + +/* + * Use a macro so that ATF_REQUIRE_MSG prints a useful line number. + */ +#define check_nspliced(sd, n) do { \ + off_t sofar; \ + \ + sofar = nspliced(sd); \ + ATF_REQUIRE_MSG(sofar == (off_t)n, "spliced %jd bytes, expected %jd", \ + (intmax_t)sofar, (intmax_t)n); \ +} while (0) + +static void +splice_init(struct splice *sp, int fd, off_t max, struct timeval *tv) +{ + memset(sp, 0, sizeof(*sp)); + sp->sp_fd = fd; + sp->sp_max = max; + if (tv != NULL) + sp->sp_idle = *tv; + else + sp->sp_idle.tv_sec = sp->sp_idle.tv_usec = 0; +} + +static void +unsplice(int fd) +{ + struct splice sp; + int error; + + splice_init(&sp, -1, 0, NULL); + error = setsockopt(fd, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); +} + +static void +unsplice_pair(int fd1, int fd2) +{ + unsplice(fd1); + unsplice(fd2); +} + +static void +splice_pair(int fd1, int fd2, off_t max, struct timeval *tv) +{ + struct splice sp; + int error; + + splice_init(&sp, fd1, max, tv); + error = setsockopt(fd2, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + splice_init(&sp, fd2, max, tv); + error = setsockopt(fd1, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); +} + +/* + * A structure representing a spliced pair of connections. left[1] is + * bidirectionally spliced with right[0]. + */ +struct splice_conn { + int left[2]; + int right[2]; +}; + +/* + * Initialize a splice connection with the given maximum number of bytes to + * splice and the given idle timeout. For now we're forced to use TCP socket, + * but at some point it would be nice (and simpler) to use pairs of PF_LOCAL + * sockets. + */ +static void +splice_conn_init_limits(struct splice_conn *sc, off_t max, struct timeval *tv) +{ + memset(sc, 0, sizeof(*sc)); + tcp4_socketpair(sc->left); + tcp4_socketpair(sc->right); + splice_pair(sc->left[1], sc->right[0], max, tv); +} + +static void +splice_conn_init(struct splice_conn *sc) +{ + splice_conn_init_limits(sc, 0, NULL); +} + +static void +splice_conn_check_empty(struct splice_conn *sc) +{ + int data; + + data = fionread(sc->left[0]); + ATF_REQUIRE_MSG(data == 0, "unexpected data on left[0]: %d", data); + data = fionread(sc->left[1]); + ATF_REQUIRE_MSG(data == 0, "unexpected data on left[1]: %d", data); + data = fionread(sc->right[0]); + ATF_REQUIRE_MSG(data == 0, "unexpected data on right[0]: %d", data); + data = fionread(sc->right[1]); + ATF_REQUIRE_MSG(data == 0, "unexpected data on right[1]: %d", data); +} + +static void +splice_conn_fini(struct splice_conn *sc) +{ + checked_close(sc->left[0]); + checked_close(sc->left[1]); + checked_close(sc->right[0]); + checked_close(sc->right[1]); +} + +static void +splice_conn_noblocking(struct splice_conn *sc) +{ + noblocking(sc->left[0]); + noblocking(sc->left[1]); + noblocking(sc->right[0]); + noblocking(sc->right[1]); +} + +/* Pass a byte through a pair of spliced connections. */ +ATF_TC_WITHOUT_HEAD(splice_basic); +ATF_TC_BODY(splice_basic, tc) +{ + struct splice_conn sc; + ssize_t n; + char c; + + splice_conn_init(&sc); + + check_nspliced(sc.left[1], 0); + check_nspliced(sc.right[0], 0); + + /* Left-to-right. */ + c = 'M'; + n = write(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 0); + + /* Right-to-left. */ + c = 'J'; + n = write(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 1); + + /* Unsplice and verify that the byte counts haven't changed. */ + unsplice(sc.left[1]); + unsplice(sc.right[0]); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 1); + + splice_conn_fini(&sc); +} + +static void +remove_rights(int fd, const cap_rights_t *toremove) +{ + cap_rights_t rights; + int error; + + error = cap_rights_get(fd, &rights); + ATF_REQUIRE_MSG(error == 0, "cap_rights_get failed: %s", + strerror(errno)); + cap_rights_remove(&rights, toremove); + error = cap_rights_limit(fd, &rights); + ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s", + strerror(errno)); +} + +/* + * Verify that splicing fails when the socket is missing the necessary rights. + */ +ATF_TC_WITHOUT_HEAD(splice_capsicum); +ATF_TC_BODY(splice_capsicum, tc) +{ + struct splice sp; + cap_rights_t rights; + off_t n; + int error, left[2], right[2]; + + tcp4_socketpair(left); + tcp4_socketpair(right); + + /* + * Make sure that we can't splice a socket that's missing recv rights. + */ + remove_rights(left[1], cap_rights_init(&rights, CAP_RECV)); + splice_init(&sp, right[0], 0, NULL); + error = setsockopt(left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCAPABLE, error == -1); + + /* Make sure we can still splice left[1] in the other direction. */ + splice_init(&sp, left[1], 0, NULL); + error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + splice_init(&sp, -1, 0, NULL); + error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + /* + * Now remove send rights from left[1] and verify that splicing is no + * longer possible. + */ + remove_rights(left[1], cap_rights_init(&rights, CAP_SEND)); + splice_init(&sp, left[1], 0, NULL); + error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCAPABLE, error == -1); + + /* + * It's still ok to query the SO_SPLICE state though. + */ + n = -1; + error = getsockopt(left[1], SOL_SOCKET, SO_SPLICE, &n, + &(socklen_t){ sizeof(n) }); + ATF_REQUIRE_MSG(error == 0, "getsockopt failed: %s", strerror(errno)); + ATF_REQUIRE(n == 0); + + /* + * Make sure that we can unsplice a spliced pair without any rights + * other than CAP_SETSOCKOPT. + */ + splice_pair(left[0], right[1], 0, NULL); + error = cap_rights_limit(left[0], + cap_rights_init(&rights, CAP_SETSOCKOPT)); + ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s", + strerror(errno)); + unsplice(left[0]); + + checked_close(left[0]); + checked_close(left[1]); + checked_close(right[0]); + checked_close(right[1]); +} + +/* + * Check various error cases in splice configuration. + */ +ATF_TC_WITHOUT_HEAD(splice_error); +ATF_TC_BODY(splice_error, tc) +{ + struct splice_conn sc; + struct splice sp; + char path[PATH_MAX]; + int error, fd, sd, usd[2]; + + memset(&sc, 0, sizeof(sc)); + tcp4_socketpair(sc.left); + tcp4_socketpair(sc.right); + + /* A negative byte limit is invalid. */ + splice_init(&sp, sc.right[0], -3, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + + /* Can't unsplice a never-spliced socket. */ + splice_init(&sp, -1, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCONN, error == -1); + + /* Can't double-unsplice a socket. */ + splice_init(&sp, sc.right[0], 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + unsplice(sc.left[1]); + splice_init(&sp, -1, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCONN, error == -1); + + /* Can't splice a spliced socket */ + splice_init(&sp, sc.right[0], 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + splice_init(&sp, sc.right[1], 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EBUSY, error == -1); + splice_init(&sp, sc.right[0], 0, NULL); + error = setsockopt(sc.left[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EBUSY, error == -1); + splice_init(&sp, -1, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + + /* Can't splice to a non-socket. */ + snprintf(path, sizeof(path), "/tmp/splice_error.XXXXXX"); + fd = mkstemp(path); + ATF_REQUIRE_MSG(fd >= 0, "mkstemp failed: %s", strerror(errno)); + splice_init(&sp, fd, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTSOCK, error == -1); + + /* Can't splice to an invalid fd. */ + checked_close(fd); + splice_init(&sp, fd, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EBADF, error == -1); + + /* Can't splice a unix stream socket. */ + error = socketpair(AF_UNIX, SOCK_STREAM, 0, usd); + ATF_REQUIRE_MSG(error == 0, "socketpair failed: %s", strerror(errno)); + splice_init(&sp, usd[0], 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EPROTONOSUPPORT, error == -1); + error = setsockopt(usd[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EPROTONOSUPPORT, error == -1); + checked_close(usd[0]); + checked_close(usd[1]); + + /* Can't splice an unconnected TCP socket. */ + sd = socket(PF_INET, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd >= 0, "socket failed: %s", strerror(errno)); + splice_init(&sp, sd, 0, NULL); + error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCONN, error == -1); + splice_init(&sp, sc.right[0], 0, NULL); + error = setsockopt(sd, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(ENOTCONN, error == -1); + + splice_conn_fini(&sc); +} + +/* + * Make sure that kevent() doesn't report read I/O events on spliced sockets. + */ +ATF_TC_WITHOUT_HEAD(splice_kevent); +ATF_TC_BODY(splice_kevent, tc) +{ + struct splice_conn sc; + struct kevent kev; + struct timespec ts; + ssize_t n; + int error, nev, kq; + uint8_t b; + + splice_conn_init(&sc); + + kq = kqueue(); + ATF_REQUIRE_MSG(kq >= 0, "kqueue failed: %s", strerror(errno)); + + EV_SET(&kev, sc.left[1], EVFILT_READ, EV_ADD, 0, 0, NULL); + error = kevent(kq, &kev, 1, NULL, 0, NULL); + ATF_REQUIRE_MSG(error == 0, "kevent failed: %s", strerror(errno)); + + memset(&ts, 0, sizeof(ts)); + nev = kevent(kq, NULL, 0, &kev, 1, &ts); + ATF_REQUIRE_MSG(nev >= 0, "kevent failed: %s", strerror(errno)); + ATF_REQUIRE(nev == 0); + + b = 'M'; + n = write(sc.left[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.right[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'M'); + + nev = kevent(kq, NULL, 0, &kev, 1, &ts); + ATF_REQUIRE_MSG(nev >= 0, "kevent failed: %s", strerror(errno)); + ATF_REQUIRE(nev == 0); + + b = 'J'; + n = write(sc.right[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'J'); + + splice_conn_fini(&sc); + checked_close(kq); +} + +/* + * Verify that a splice byte limit is applied. + */ +ATF_TC_WITHOUT_HEAD(splice_limit_bytes); +ATF_TC_BODY(splice_limit_bytes, tc) +{ + struct splice_conn sc; + ssize_t n; + uint8_t b, buf[128]; + + splice_conn_init_limits(&sc, sizeof(buf) + 1, NULL); + + memset(buf, 'A', sizeof(buf)); + for (size_t total = sizeof(buf); total > 0; total -= n) { + n = write(sc.left[0], buf, total); + ATF_REQUIRE_MSG(n > 0, "write failed: %s", strerror(errno)); + } + for (size_t total = sizeof(buf); total > 0; total -= n) { + n = read(sc.right[1], buf, sizeof(buf)); + ATF_REQUIRE_MSG(n > 0, "read failed: %s", strerror(errno)); + } + + check_nspliced(sc.left[1], sizeof(buf)); + check_nspliced(sc.right[0], 0); + + /* Trigger an unsplice by writing the last byte. */ + b = 'B'; + n = write(sc.left[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.right[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'B'); + + /* + * The next byte should appear on the other side of the connection + * rather than the splice. + */ + b = 'C'; + n = write(sc.left[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'C'); + + splice_conn_check_empty(&sc); + + splice_conn_fini(&sc); +} + +/* + * Verify that a splice timeout limit is applied. + */ +ATF_TC_WITHOUT_HEAD(splice_limit_timeout); +ATF_TC_BODY(splice_limit_timeout, tc) +{ + struct splice_conn sc; + ssize_t n; + int error; + uint8_t b, buf[128]; + + splice_conn_init_limits(&sc, 0, + &(struct timeval){ .tv_sec = 0, .tv_usec = 500000 /* 500ms */ }); + + /* Write some data through the splice. */ + memset(buf, 'A', sizeof(buf)); + for (size_t total = sizeof(buf); total > 0; total -= n) { + n = write(sc.left[0], buf, total); + ATF_REQUIRE_MSG(n > 0, "write failed: %s", strerror(errno)); + } + for (size_t total = sizeof(buf); total > 0; total -= n) { + n = read(sc.right[1], buf, sizeof(buf)); + ATF_REQUIRE_MSG(n > 0, "read failed: %s", strerror(errno)); + } + + check_nspliced(sc.left[1], sizeof(buf)); + check_nspliced(sc.right[0], 0); + + /* Wait for the splice to time out. */ + error = usleep(550000); + ATF_REQUIRE_MSG(error == 0, "usleep failed: %s", strerror(errno)); + + /* + * The next byte should appear on the other side of the connection + * rather than the splice. + */ + b = 'C'; + n = write(sc.left[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'C'); + + splice_conn_fini(&sc); +} + +/* + * Make sure that listen() fails on spliced sockets, and that SO_SPLICE can't be + * used with listening sockets. + */ +ATF_TC_WITHOUT_HEAD(splice_listen); +ATF_TC_BODY(splice_listen, tc) +{ + struct splice sp; + struct splice_conn sc; + int error, sd[3]; + + /* + * These should fail regardless since the sockets are connected, but it + * doesn't hurt to check. + */ + splice_conn_init(&sc); + error = listen(sc.left[1], 1); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + error = listen(sc.right[0], 1); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + splice_conn_fini(&sc); + + tcp4_socketpair(sd); + sd[2] = socket(PF_INET, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd[2] >= 0, "socket failed: %s", strerror(errno)); + error = listen(sd[2], 1); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + /* + * Make sure a listening socket can't be spliced in either direction. + */ + splice_init(&sp, sd[2], 0, NULL); + error = setsockopt(sd[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + splice_init(&sp, sd[1], 0, NULL); + error = setsockopt(sd[2], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + + /* + * Make sure we can't try to unsplice a listening socket. + */ + splice_init(&sp, -1, 0, NULL); + error = setsockopt(sd[2], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + + checked_close(sd[0]); + checked_close(sd[1]); + checked_close(sd[2]); +} + +static void +sigalarm(int sig __unused) +{ +} + +/* + * Our SO_SPLICE implementation doesn't do anything to prevent loops. We should + * however make sure that they are interruptible. + */ +ATF_TC_WITHOUT_HEAD(splice_loop); +ATF_TC_BODY(splice_loop, tc) +{ + ssize_t n; + int sd[2], status; + pid_t child; + char c; + + tcp_socketpair(sd, PF_INET); + splice_pair(sd[0], sd[1], 0, NULL); + + /* + * Let the child process trigger an infinite loop. It should still be + * possible to kill the child with a signal, causing the connection to + * be dropped and ending the loop. + */ + child = fork(); + ATF_REQUIRE_MSG(child >= 0, "fork failed: %s", strerror(errno)); + if (child == 0) { + alarm(2); + c = 42; + n = write(sd[0], &c, 1); + if (n != 1) + _exit(2); + c = 24; + n = write(sd[1], &c, 1); + if (n != 1) + _exit(3); + + for (;;) { + /* Wait for SIGALARM. */ + sleep(100); + } + + _exit(0); + } else { + checked_close(sd[0]); + checked_close(sd[1]); + + child = waitpid(child, &status, 0); + ATF_REQUIRE_MSG(child >= 0, + "waitpid failed: %s", strerror(errno)); + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE(WTERMSIG(status) == SIGALRM); + } +} + +/* + * Simple I/O test. + */ +ATF_TC_WITHOUT_HEAD(splice_nonblock); +ATF_TC_BODY(splice_nonblock, tc) +{ + struct splice_conn sc; + char buf[200]; + size_t sofar; + ssize_t n; + + splice_conn_init(&sc); + splice_conn_noblocking(&sc); + + memset(buf, 'A', sizeof(buf)); + for (sofar = 0;;) { + n = write(sc.left[0], buf, sizeof(buf)); + if (n < 0) { + ATF_REQUIRE_ERRNO(EAGAIN, n == -1); + break; + } + sofar += n; + } + + while (sofar > 0) { + n = read(sc.right[1], buf, sizeof(buf)); + if (n < 0) { + ATF_REQUIRE_ERRNO(EAGAIN, n == -1); + usleep(100); + } else { + for (size_t i = 0; i < (size_t)n; i++) + ATF_REQUIRE(buf[i] == 'A'); + sofar -= n; + } + } + + splice_conn_fini(&sc); +} + +ATF_TC_WITHOUT_HEAD(splice_resplice); +ATF_TC_BODY(splice_resplice, tc) +{ + struct splice_conn sc; + ssize_t n; + char c; + + splice_conn_init(&sc); + + /* Left-to-right. */ + c = 'M'; + n = write(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 0); + + /* Right-to-left. */ + c = 'J'; + n = write(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 1); + + /* Unsplice and verify that the byte counts haven't changed. */ + unsplice(sc.left[1]); + unsplice(sc.right[0]); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 1); + + /* Splice again, check that byte counts are reset. */ + splice_pair(sc.left[1], sc.right[0], 0, NULL); + check_nspliced(sc.left[1], 0); + check_nspliced(sc.right[0], 0); + + /* Left-to-right. */ + c = 'M'; + n = write(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 0); + + /* Right-to-left. */ + c = 'J'; + n = write(sc.right[1], &c, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sc.left[0], &c, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c); + check_nspliced(sc.left[1], 1); + check_nspliced(sc.right[0], 1); + + splice_conn_fini(&sc); +} + +struct xfer_args { + pthread_barrier_t *barrier; + uint32_t bytes; + int fd; +}; + +static void * +xfer(void *arg) +{ + struct xfer_args *xfer; + uint8_t *buf; + size_t sz; + ssize_t n; + uint32_t resid; + int error; + + xfer = arg; + + error = fcntl(xfer->fd, F_SETFL, O_NONBLOCK); + ATF_REQUIRE_MSG(error == 0, "fcntl failed: %s", strerror(errno)); + + sz = MIN(xfer->bytes, 1024 * 1024); + buf = malloc(sz); + ATF_REQUIRE(buf != NULL); + arc4random_buf(buf, sz); + + pthread_barrier_wait(xfer->barrier); + + for (resid = xfer->bytes; xfer->bytes > 0 || resid > 0;) { + n = write(xfer->fd, buf, MIN(sz, xfer->bytes)); + if (n < 0) { + ATF_REQUIRE_ERRNO(EAGAIN, n == -1); + usleep(1000); + } else { + ATF_REQUIRE(xfer->bytes >= (size_t)n); + xfer->bytes -= n; + } + + n = read(xfer->fd, buf, sz); + if (n < 0) { + ATF_REQUIRE_ERRNO(EAGAIN, n == -1); + usleep(1000); + } else { + ATF_REQUIRE(resid >= (size_t)n); + resid -= n; + } + } + + free(buf); + return (NULL); +} + +/* + * Use two threads to transfer data between two spliced connections. + */ +ATF_TC_WITHOUT_HEAD(splice_throughput); +ATF_TC_BODY(splice_throughput, tc) +{ + struct xfer_args xfers[2]; + pthread_t thread[2]; + pthread_barrier_t barrier; + struct splice_conn sc; + uint32_t bytes; + int error; + + /* Transfer an amount between 1B and 1GB. */ + bytes = arc4random_uniform(1024 * 1024 * 1024) + 1; + splice_conn_init(&sc); + + error = pthread_barrier_init(&barrier, NULL, 2); + ATF_REQUIRE(error == 0); + xfers[0] = (struct xfer_args){ + .barrier = &barrier, + .bytes = bytes, + .fd = sc.left[0] + }; + xfers[1] = (struct xfer_args){ + .barrier = &barrier, + .bytes = bytes, + .fd = sc.right[1] + }; + + error = pthread_create(&thread[0], NULL, xfer, &xfers[0]); + ATF_REQUIRE_MSG(error == 0, + "pthread_create failed: %s", strerror(errno)); + error = pthread_create(&thread[1], NULL, xfer, &xfers[1]); + ATF_REQUIRE_MSG(error == 0, + "pthread_create failed: %s", strerror(errno)); + + error = pthread_join(thread[0], NULL); + ATF_REQUIRE_MSG(error == 0, + "pthread_join failed: %s", strerror(errno)); + error = pthread_join(thread[1], NULL); + ATF_REQUIRE_MSG(error == 0, + "pthread_join failed: %s", strerror(errno)); + + error = pthread_barrier_destroy(&barrier); + ATF_REQUIRE(error == 0); + splice_conn_fini(&sc); +} + +/* + * Make sure it's possible to splice v4 and v6 sockets together. + */ +ATF_TC_WITHOUT_HEAD(splice_v4v6); +ATF_TC_BODY(splice_v4v6, tc) +{ + struct splice sp; + ssize_t n; + int sd4[2], sd6[2]; + int error; + uint8_t b; + + tcp4_socketpair(sd4); + tcp6_socketpair(sd6); + + splice_init(&sp, sd6[0], 0, NULL); + error = setsockopt(sd4[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + splice_init(&sp, sd4[1], 0, NULL); + error = setsockopt(sd6[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + b = 'M'; + n = write(sd4[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sd6[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'M'); + + b = 'J'; + n = write(sd6[1], &b, 1); + ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno)); + n = read(sd4[0], &b, 1); + ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno)); + ATF_REQUIRE(b == 'J'); + + checked_close(sd4[0]); + checked_close(sd4[1]); + checked_close(sd6[0]); + checked_close(sd6[1]); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, splice_basic); + ATF_TP_ADD_TC(tp, splice_capsicum); + ATF_TP_ADD_TC(tp, splice_error); + ATF_TP_ADD_TC(tp, splice_kevent); + ATF_TP_ADD_TC(tp, splice_limit_bytes); + ATF_TP_ADD_TC(tp, splice_limit_timeout); + ATF_TP_ADD_TC(tp, splice_listen); + ATF_TP_ADD_TC(tp, splice_loop); + ATF_TP_ADD_TC(tp, splice_nonblock); + ATF_TP_ADD_TC(tp, splice_resplice); + ATF_TP_ADD_TC(tp, splice_throughput); + ATF_TP_ADD_TC(tp, splice_v4v6); + return (atf_no_error()); +} diff --git a/tests/sys/kern/sysctl_security_jail_children.sh b/tests/sys/kern/sysctl_security_jail_children.sh new file mode 100644 index 000000000000..ac990e57ff74 --- /dev/null +++ b/tests/sys/kern/sysctl_security_jail_children.sh @@ -0,0 +1,77 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +atf_test_case "max_cur" "cleanup" +max_cur_head() +{ + atf_set descr 'Test maximum and current number of child jails' + atf_set require.user root + atf_set execenv jail +} +max_cur_body() +{ + origin_max=$(sysctl -n security.jail.children.max) + origin_cur=$(sysctl -n security.jail.children.cur) + + # Magic numbers reasoning: + # 3 stands for: + # - the test creates three jails: childfree, maxallowed, maxallowed.family + # 6 stands for: + # - maxallowed.family wants to set children.max=4 + # - it means that its parent (maxallowed) should have at least children.max=5 + # - it makes the origin (parent of maxallowed) provide children.max=6 minimum + # + test $origin_cur -le $origin_max || atf_fail "Abnormal cur=$origin_cur > max=$origin_max." + test $((origin_max - origin_cur)) -ge 3 || atf_skip "Not enough child jails are allowed for the test." + test $origin_max -ge 6 || atf_skip "Not high enough children.max limit for the test." + + jail -c name=childfree persist + atf_check_equal "$((origin_cur + 1))" "$(sysctl -n security.jail.children.cur)" + atf_check_equal "0" "$(jexec childfree sysctl -n security.jail.children.max)" + atf_check_equal "0" "$(jexec childfree sysctl -n security.jail.children.cur)" + + jail -c name=maxallowed children.max=$((origin_max - 1)) persist + atf_check_equal "$((origin_cur + 2))" "$(sysctl -n security.jail.children.cur)" + atf_check_equal "$((origin_max - 1))" "$(jexec maxallowed sysctl -n security.jail.children.max)" + atf_check_equal "0" "$(jexec maxallowed sysctl -n security.jail.children.cur)" + + jexec maxallowed jail -c name=family children.max=4 persist + atf_check_equal "$((origin_cur + 3))" "$(sysctl -n security.jail.children.cur)" + atf_check_equal "1" "$(jexec maxallowed sysctl -n security.jail.children.cur)" + atf_check_equal "4" "$(jexec maxallowed.family sysctl -n security.jail.children.max)" + atf_check_equal "0" "$(jexec maxallowed.family sysctl -n security.jail.children.cur)" +} +max_cur_cleanup() +{ + jail -r maxallowed + jail -r childfree + return 0 +} + +atf_init_test_cases() +{ + atf_add_test_case "max_cur" +} diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile new file mode 100644 index 000000000000..8628ab79875f --- /dev/null +++ b/tests/sys/kern/tty/Makefile @@ -0,0 +1,15 @@ +TESTSDIR= ${TESTSBASE}/sys/kern/tty +BINDIR= ${TESTSDIR} + +PLAIN_TESTS_PORCH+= test_canon +PLAIN_TESTS_PORCH+= test_canon_fullbuf +PLAIN_TESTS_PORCH+= test_ncanon +PLAIN_TESTS_PORCH+= test_recanon +ATF_TESTS_C+= test_sti + +PROGS+= fionread +PROGS+= readsz + +LIBADD.test_sti= util + +.include <bsd.test.mk> diff --git a/tests/sys/kern/tty/fionread.c b/tests/sys/kern/tty/fionread.c new file mode 100644 index 000000000000..929d613f883b --- /dev/null +++ b/tests/sys/kern/tty/fionread.c @@ -0,0 +1,21 @@ +/*- + * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/ioctl.h> + +#include <assert.h> +#include <stdio.h> +#include <unistd.h> + +int +main(void) +{ + int nb; + + assert(ioctl(STDIN_FILENO, FIONREAD, &nb) == 0); + printf("%d", nb); + return (0); +} diff --git a/tests/sys/kern/tty/readsz.c b/tests/sys/kern/tty/readsz.c new file mode 100644 index 000000000000..95dafa02472f --- /dev/null +++ b/tests/sys/kern/tty/readsz.c @@ -0,0 +1,130 @@ +/*- + * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/param.h> + +#include <err.h> +#include <errno.h> +#include <limits.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +static void +usage(void) +{ + + fprintf(stderr, "usage: %s [-b bytes | -c lines | -e] [-s buffer-size]\n", + getprogname()); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + char *buf; + const char *errstr; + size_t bufsz = 0, reps; + ssize_t ret; + enum { MODE_BYTES, MODE_COUNT, MODE_EOF } mode; + int ch; + + /* + * -b specifies number of bytes. + * -c specifies number of read() calls. + * -e specifies eof (default) + * -s to pass a buffer size + * + * Reading N lines is the same as -c with a high buffer size. + */ + mode = MODE_EOF; + while ((ch = getopt(argc, argv, "b:c:es:")) != -1) { + switch (ch) { + case 'b': + mode = MODE_BYTES; + reps = strtonum(optarg, 0, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + case 'c': + mode = MODE_COUNT; + reps = strtonum(optarg, 1, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + case 'e': + mode = MODE_EOF; + break; + case 's': + bufsz = strtonum(optarg, 1, SSIZE_MAX, &errstr); + if (errstr != NULL) + errx(1, "strtonum: %s", errstr); + break; + default: + usage(); + } + } + + if (bufsz == 0) { + if (mode == MODE_BYTES) + bufsz = reps; + else + bufsz = LINE_MAX; + } + + buf = malloc(bufsz); + if (buf == NULL) + err(1, "malloc"); + + for (;;) { + size_t readsz; + + /* + * Be careful not to over-read if we're in byte-mode. In every other + * mode, we'll read as much as we can. + */ + if (mode == MODE_BYTES) + readsz = MIN(bufsz, reps); + else + readsz = bufsz; + + ret = read(STDIN_FILENO, buf, readsz); + if (ret == -1 && errno == EINTR) + continue; + if (ret == -1) + err(1, "read"); + if (ret == 0) { + if (mode == MODE_EOF) + return (0); + errx(1, "premature EOF"); + } + + /* Write out what we've got */ + write(STDOUT_FILENO, buf, ret); + + /* + * Bail out if we've hit our metric (byte mode / count mode). + */ + switch (mode) { + case MODE_BYTES: + reps -= ret; + if (reps == 0) + return (0); + break; + case MODE_COUNT: + reps--; + if (reps == 0) + return (0); + break; + default: + break; + } + } + + return (0); +} diff --git a/tests/sys/kern/tty/test_canon.orch b/tests/sys/kern/tty/test_canon.orch new file mode 100644 index 000000000000..28018edfdcd6 --- /dev/null +++ b/tests/sys/kern/tty/test_canon.orch @@ -0,0 +1,102 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +spawn("cat") + +write "Complete\r" +match "Complete\r" + +write "Basic\rIncomplete" +match "Basic\r" + +-- We shouldn't see any of the "Incomplete" line +fail(function() +end) + +match "Incomp" { + callback = function() + exit(1) + end +} + +fail(nil) + +-- Pushing a ^D along should force a flush of the tty, cat(1) will write the +-- result without a trailing newline. +write " line^D" +match "Incomplete line$" + +-- Erase! +write "Dog^H^D" +match "Do$" + +-- More erase! +write "Cat Dog^W^D" +match "Cat $" + +write "^D" +eof() + +local function fionread_test(str, expected) + spawn("fionread") + + write(str) + match(expected) +end + +-- Incomplete line +fionread_test("Hello", "0") +-- VEOF does not count +fionread_test("Hello^D", "5") +-- VEOF still doesn't count, even if the next line is an extra VEOF later +fionread_test("Hello^D^D", "5") +-- read(2) definitely won't return the second incomplete line +fionread_test("Hello^Dther", "5") +-- read(2) also won't return a second complete line at once +fionread_test("Hello^Dthere^D", "5") +-- Finally, send a VEOF to terminate a blank line and signal EOF in read(2) +fionread_test("^D", "0") + +-- \r will instead show up in the input stream to the application, so we must +-- make sure those are counted where VEOF generally wouldn't be. +fionread_test("Hello\r", "6") +fionread_test("Hello\rther", "6") +fionread_test("Hello\rthere\r", "6") +fionread_test("\r", "1") + +local function readsz_test(str, arg, expected) + spawn("readsz", table.unpack(arg)) + + if type(str) == "table" then + assert(#str == 2) + write(str[1]) + release() + + -- Give readsz a chance to consume the partial input before we send more + -- along. + sleep(1) + write(str[2]) + else + write(str) + end + match(expected) +end + +readsz_test("partial", {"-b", 3}, "^$") +readsz_test("partial^D", {"-b", 3}, "^par$") +readsz_test("partial^D", {"-c", 1}, "^partial$") +for s = 1, #"partial" do + readsz_test("partial^D", {"-s", s}, "^partial$") +end +-- Send part of the line, release and pause, then finish it. +readsz_test({"par", "tial^D"}, {"-c", 1}, "^partial$") +-- line is incomplete, so we'll just see the "partial" even if we want two +readsz_test("partial^Dline", {"-c", 2}, "^partial$") +readsz_test("partial^Dline^D", {"-c", 1}, "^partial$") +readsz_test("partial^Dline^D", {"-c", 2}, "^partialline$") diff --git a/tests/sys/kern/tty/test_canon_fullbuf.orch b/tests/sys/kern/tty/test_canon_fullbuf.orch new file mode 100644 index 000000000000..1833703e4f45 --- /dev/null +++ b/tests/sys/kern/tty/test_canon_fullbuf.orch @@ -0,0 +1,23 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local TTYINQ_DATASIZE = 128 +local scream = string.rep("A", TTYINQ_DATASIZE - 1) + +spawn("cat") + +-- Fill up a whole block with screaming + VEOF +write(scream .. "^D") +match(scream .. "$") + +scream = scream .. "A" + +-- Now fill up the next block, but spill the VEOF over to a third block. +write(scream .. "^D") +match(scream .. "$") diff --git a/tests/sys/kern/tty/test_ncanon.orch b/tests/sys/kern/tty/test_ncanon.orch new file mode 100644 index 000000000000..14a34d82fa9a --- /dev/null +++ b/tests/sys/kern/tty/test_ncanon.orch @@ -0,0 +1,39 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local function spawn_one(...) + spawn(...) + + stty("lflag", 0, tty.lflag.ICANON) +end + +-- We can send one byte... +spawn_one("readsz", "-c", 1) +write "H" +match "^H$" + +-- or many. +spawn_one("readsz", "-c", 1) +write "Hello" +match "^Hello$" + +-- VEOF is a normal character here, passed through as-is. +spawn_one("readsz", "-c", 1) +write "Hello^D" +match "^Hello\x04$" +spawn_one("readsz", "-c", 1) +write "^D" +match "^\x04$" + +-- Confirm that FIONREAD agrees that VEOF will be returned, even if it was sent +-- while the tty was still in canonical mode. +spawn("fionread") +write "^D" +stty("lflag", 0, tty.lflag.ICANON) +match "^1$" diff --git a/tests/sys/kern/tty/test_recanon.orch b/tests/sys/kern/tty/test_recanon.orch new file mode 100644 index 000000000000..e3943495ca5d --- /dev/null +++ b/tests/sys/kern/tty/test_recanon.orch @@ -0,0 +1,90 @@ +#!/usr/bin/env -S porch -f +-- +-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> +-- +-- SPDX-License-Identifier: BSD-2-Clause +-- + +timeout(3) + +local TTYINQ_DATASIZE = 128 +local scream = string.rep("A", TTYINQ_DATASIZE - 1) + +local function ncanon() + stty("lflag", nil, tty.lflag.ICANON) +end + +local function canon() + stty("lflag", tty.lflag.ICANON) +end + +spawn("readsz", "-e") +ncanon() + +-- Fill up a whole block with screaming + VEOF; when it gets recanonicalized, +-- the next line should be pointing to the beginning of the next block. +write(scream .. "^D") + +canon() +match(scream .. "$") + +-- The same as above, but spilling VEOF over to the next block. +spawn("readsz", "-e") +ncanon() + +write(scream .. "A^D") + +canon() +match(scream .. "A$") + +-- We'll do it again, except with one character spilled over to the next block +-- before we recanonicalize. We should then have the scream, followed by a +-- partial line containing the spill over. +spawn("cat") +ncanon() + +write(scream .. "^DZ") + +canon() +match(scream .. "$") + +-- Sending "B^D" should give us "ZB" to make sure that we didn't lose anything +-- at the beginning of the next block. + +write("B^D") +match("^ZB$") + +-- Next we'll VEOF at the beginning. +spawn("readsz", "-e") +ncanon() + +write("^D") +match("^$") + +-- Finally, we'll trigger recanonicalization with an empty buffer. This one is +-- just about avoiding a panic. +spawn("true") + +ncanon() +canon() +release() +eof() + +spawn("readsz", "-c", "1") + +write("Test^Dfoo") +ncanon() + +match("^Test\x04foo$") + +-- Finally, swap VEOF out with ^F; before recent changes, we would remain +-- canonicalized at Test^D and the kernel would block on it unless a short +-- buffer was used since VEOF would not appear within the canonicalized bit. +spawn("readsz", "-c", 1) + +write("Test^DLine^F") +stty("cc", { + VEOF = "^F" +}) + +match("^Test\x04Line$") diff --git a/tests/sys/kern/tty/test_sti.c b/tests/sys/kern/tty/test_sti.c new file mode 100644 index 000000000000..f792001b4e3f --- /dev/null +++ b/tests/sys/kern/tty/test_sti.c @@ -0,0 +1,337 @@ +/*- + * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/param.h> +#include <sys/ioctl.h> +#include <sys/wait.h> + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <signal.h> +#include <stdbool.h> +#include <stdlib.h> +#include <termios.h> + +#include <atf-c.h> +#include <libutil.h> + +enum stierr { + STIERR_CONFIG_FETCH, + STIERR_CONFIG, + STIERR_INJECT, + STIERR_READFAIL, + STIERR_BADTEXT, + STIERR_DATAFOUND, + STIERR_ROTTY, + STIERR_WOTTY, + STIERR_WOOK, + STIERR_BADERR, + + STIERR_MAXERR +}; + +static const struct stierr_map { + enum stierr stierr; + const char *msg; +} stierr_map[] = { + { STIERR_CONFIG_FETCH, "Failed to fetch ctty configuration" }, + { STIERR_CONFIG, "Failed to configure ctty in the child" }, + { STIERR_INJECT, "Failed to inject characters via TIOCSTI" }, + { STIERR_READFAIL, "Failed to read(2) from stdin" }, + { STIERR_BADTEXT, "read(2) data did not match injected data" }, + { STIERR_DATAFOUND, "read(2) data when we did not expected to" }, + { STIERR_ROTTY, "Failed to open tty r/o" }, + { STIERR_WOTTY, "Failed to open tty w/o" }, + { STIERR_WOOK, "TIOCSTI on w/o tty succeeded" }, + { STIERR_BADERR, "Received wrong error from failed TIOCSTI" }, +}; +_Static_assert(nitems(stierr_map) == STIERR_MAXERR, + "Failed to describe all errors"); + +/* + * Inject each character of the input string into the TTY. The caller can + * assume that errno is preserved on return. + */ +static ssize_t +inject(int fileno, const char *str) +{ + size_t nb = 0; + + for (const char *walker = str; *walker != '\0'; walker++) { + if (ioctl(fileno, TIOCSTI, walker) != 0) + return (-1); + nb++; + } + + return (nb); +} + +/* + * Forks off a new process, stashes the parent's handle for the pty in *termfd + * and returns the pid. 0 for the child, >0 for the parent, as usual. + * + * Most tests fork so that we can do them while unprivileged, which we can only + * do if we're operating on our ctty (and we don't want to touch the tty of + * whatever may be running the tests). + */ +static int +init_pty(int *termfd, bool canon) +{ + int pid; + + pid = forkpty(termfd, NULL, NULL, NULL); + ATF_REQUIRE(pid != -1); + + if (pid == 0) { + struct termios term; + + /* + * Child reconfigures tty to disable echo and put it into raw + * mode if requested. + */ + if (tcgetattr(STDIN_FILENO, &term) == -1) + _exit(STIERR_CONFIG_FETCH); + term.c_lflag &= ~ECHO; + if (!canon) + term.c_lflag &= ~ICANON; + if (tcsetattr(STDIN_FILENO, TCSANOW, &term) == -1) + _exit(STIERR_CONFIG); + } + + return (pid); +} + +static void +finalize_child(pid_t pid, int signo) +{ + int status, wpid; + + while ((wpid = waitpid(pid, &status, 0)) != pid) { + if (wpid != -1) + continue; + ATF_REQUIRE_EQ_MSG(EINTR, errno, + "waitpid: %s", strerror(errno)); + } + + /* + * Some tests will signal the child for whatever reason, and we're + * expecting it to terminate it. For those cases, it's OK to just see + * that termination. For all other cases, we expect a graceful exit + * with an exit status that reflects a cause that we have an error + * mapped for. + */ + if (signo >= 0) { + ATF_REQUIRE(WIFSIGNALED(status)); + ATF_REQUIRE_EQ(signo, WTERMSIG(status)); + } else { + ATF_REQUIRE(WIFEXITED(status)); + if (WEXITSTATUS(status) != 0) { + int err = WEXITSTATUS(status); + + for (size_t i = 0; i < nitems(stierr_map); i++) { + const struct stierr_map *map = &stierr_map[i]; + + if ((int)map->stierr == err) { + atf_tc_fail("%s", map->msg); + __assert_unreachable(); + } + } + } + } +} + +ATF_TC(basic); +ATF_TC_HEAD(basic, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test for basic functionality of TIOCSTI"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(basic, tc) +{ + int pid, term; + + /* + * We don't canonicalize on this test because we can assume that the + * injected data will be available after TIOCSTI returns. This is all + * within a single thread for the basic test, so we simplify our lives + * slightly in raw mode. + */ + pid = init_pty(&term, false); + if (pid == 0) { + static const char sending[] = "Text"; + char readbuf[32]; + ssize_t injected, readsz; + + injected = inject(STDIN_FILENO, sending); + if (injected != sizeof(sending) - 1) + _exit(STIERR_INJECT); + + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + + if (readsz < 0 || readsz != injected) + _exit(STIERR_READFAIL); + if (memcmp(readbuf, sending, readsz) != 0) + _exit(STIERR_BADTEXT); + + _exit(0); + } + + finalize_child(pid, -1); +} + +ATF_TC(root); +ATF_TC_HEAD(root, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that root can inject into another TTY"); + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(root, tc) +{ + static const char sending[] = "Text\r"; + ssize_t injected; + int pid, term; + + /* + * We leave canonicalization enabled for this one so that the read(2) + * below hangs until we have all of the data available, rather than + * having to signal OOB that it's safe to read. + */ + pid = init_pty(&term, true); + if (pid == 0) { + char readbuf[32]; + ssize_t readsz; + + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + if (readsz < 0 || readsz != sizeof(sending) - 1) + _exit(STIERR_READFAIL); + + /* + * Here we ignore the trailing \r, because it won't have + * surfaced in our read(2). + */ + if (memcmp(readbuf, sending, readsz - 1) != 0) + _exit(STIERR_BADTEXT); + + _exit(0); + } + + injected = inject(term, sending); + ATF_REQUIRE_EQ_MSG(sizeof(sending) - 1, injected, + "Injected %zu characters, expected %zu", injected, + sizeof(sending) - 1); + + finalize_child(pid, -1); +} + +ATF_TC(unprivileged_fail_noctty); +ATF_TC_HEAD(unprivileged_fail_noctty, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that unprivileged cannot inject into non-controlling TTY"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(unprivileged_fail_noctty, tc) +{ + const char sending[] = "Text"; + ssize_t injected; + int pid, serrno, term; + + pid = init_pty(&term, false); + if (pid == 0) { + char readbuf[32]; + ssize_t readsz; + + /* + * This should hang until we get terminated by the parent. + */ + readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf)); + if (readsz > 0) + _exit(STIERR_DATAFOUND); + + _exit(0); + } + + /* Should fail. */ + injected = inject(term, sending); + serrno = errno; + + /* Done with the child, just kill it now to avoid problems later. */ + kill(pid, SIGINT); + finalize_child(pid, SIGINT); + + ATF_REQUIRE_EQ_MSG(-1, (ssize_t)injected, + "TIOCSTI into non-ctty succeeded"); + ATF_REQUIRE_EQ(EACCES, serrno); +} + +ATF_TC(unprivileged_fail_noread); +ATF_TC_HEAD(unprivileged_fail_noread, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Test that unprivileged cannot inject into TTY not opened for read"); + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(unprivileged_fail_noread, tc) +{ + int pid, term; + + /* + * Canonicalization actually doesn't matter for this one, we'll trust + * that the failure means we didn't inject anything. + */ + pid = init_pty(&term, true); + if (pid == 0) { + static const char sending[] = "Text"; + ssize_t injected; + int rotty, wotty; + + /* + * We open the tty both r/o and w/o to ensure we got the device + * name right; one of these will pass, one of these will fail. + */ + wotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_WRONLY); + if (wotty == -1) + _exit(STIERR_WOTTY); + rotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_RDONLY); + if (rotty == -1) + _exit(STIERR_ROTTY); + + /* + * This injection is expected to fail with EPERM, because it may + * be our controlling tty but it is not open for reading. + */ + injected = inject(wotty, sending); + if (injected != -1) + _exit(STIERR_WOOK); + if (errno != EPERM) + _exit(STIERR_BADERR); + + /* + * Demonstrate that it does succeed on the other fd we opened, + * which is r/o. + */ + injected = inject(rotty, sending); + if (injected != sizeof(sending) - 1) + _exit(STIERR_INJECT); + + _exit(0); + } + + finalize_child(pid, -1); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, basic); + ATF_TP_ADD_TC(tp, root); + ATF_TP_ADD_TC(tp, unprivileged_fail_noctty); + ATF_TP_ADD_TC(tp, unprivileged_fail_noread); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/unix_dgram.c b/tests/sys/kern/unix_dgram.c index 9df0d4ca7168..7464cdf197cd 100644 --- a/tests/sys/kern/unix_dgram.c +++ b/tests/sys/kern/unix_dgram.c @@ -25,13 +25,15 @@ * SUCH DAMAGE. */ -#include <sys/time.h> #include <sys/event.h> #include <sys/ioctl.h> #include <sys/select.h> #include <sys/socket.h> +#include <sys/stat.h> #include <sys/sysctl.h> +#include <sys/time.h> #include <sys/un.h> + #include <aio.h> #include <errno.h> #include <fcntl.h> @@ -391,12 +393,49 @@ ATF_TC_BODY(selfgetpeername, tc) ATF_REQUIRE(close(sd) == 0); } +ATF_TC_WITHOUT_HEAD(fchmod); +ATF_TC_BODY(fchmod, tc) +{ + struct stat sb; + struct sockaddr_un sun; + int error, sd; + + memset(&sun, 0, sizeof(sun)); + sun.sun_len = sizeof(sun); + sun.sun_family = AF_UNIX; + strlcpy(sun.sun_path, "sock", sizeof(sun.sun_path)); + + sd = socket(PF_UNIX, SOCK_DGRAM, 0); + ATF_REQUIRE(sd != -1); + + error = fchmod(sd, 0600 | S_ISUID); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + + umask(0022); + error = fchmod(sd, 0766); + ATF_REQUIRE(error == 0); + + error = bind(sd, (struct sockaddr *)&sun, sizeof(sun)); + ATF_REQUIRE(error == 0); + + error = stat(sun.sun_path, &sb); + ATF_REQUIRE(error == 0); + ATF_REQUIRE_MSG((sb.st_mode & 0777) == 0744, + "sb.st_mode = %o", sb.st_mode); + + error = fchmod(sd, 0666); + ATF_REQUIRE_ERRNO(EINVAL, error == -1); + + ATF_REQUIRE(close(sd) == 0); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, basic); ATF_TP_ADD_TC(tp, one2many); ATF_TP_ADD_TC(tp, event); ATF_TP_ADD_TC(tp, selfgetpeername); + ATF_TP_ADD_TC(tp, fchmod); return (atf_no_error()); } diff --git a/tests/sys/kern/unix_passfd_test.c b/tests/sys/kern/unix_passfd_test.c index 74095859d899..95271c04a16b 100644 --- a/tests/sys/kern/unix_passfd_test.c +++ b/tests/sys/kern/unix_passfd_test.c @@ -27,15 +27,19 @@ */ #include <sys/param.h> +#include <sys/jail.h> #include <sys/socket.h> #include <sys/stat.h> #include <sys/sysctl.h> #include <sys/time.h> #include <sys/resource.h> #include <sys/un.h> +#include <sys/wait.h> +#include <err.h> #include <errno.h> #include <fcntl.h> +#include <jail.h> #include <limits.h> #include <stdio.h> #include <stdlib.h> @@ -544,6 +548,51 @@ ATF_TC_BODY(send_overflow, tc) closesocketpair(fd); } +/* + * Make sure that we do not receive descriptors with MSG_PEEK. + */ +ATF_TC_WITHOUT_HEAD(peek); +ATF_TC_BODY(peek, tc) +{ + int fd[2], getfd, putfd, nfds; + + domainsocketpair(fd); + tempfile(&putfd); + nfds = getnfds(); + sendfd(fd[0], putfd); + ATF_REQUIRE(getnfds() == nfds); + + /* First make MSG_PEEK recvmsg(2)... */ + char cbuf[CMSG_SPACE(sizeof(int))]; + char buf[1]; + struct iovec iov = { + .iov_base = buf, + .iov_len = sizeof(buf) + }; + struct msghdr msghdr = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf), + }; + ATF_REQUIRE(1 == recvmsg(fd[1], &msghdr, MSG_PEEK)); + for (struct cmsghdr *cmsghdr = CMSG_FIRSTHDR(&msghdr); + cmsghdr != NULL; cmsghdr = CMSG_NXTHDR(&msghdr, cmsghdr)) { + /* Usually this is some garbage. */ + printf("level %d type %d len %u\n", + cmsghdr->cmsg_level, cmsghdr->cmsg_type, cmsghdr->cmsg_len); + } + + /* ... and make sure we did not receive any descriptors! */ + ATF_REQUIRE(getnfds() == nfds); + + /* Now really receive a descriptor. */ + recvfd(fd[1], &getfd, 0); + ATF_REQUIRE(getnfds() == nfds + 1); + close(putfd); + close(getfd); + closesocketpair(fd); +} /* * Send two files. Then receive them. Make sure they are returned in the @@ -987,6 +1036,135 @@ ATF_TC_BODY(control_creates_records, tc) closesocketpair(fd); } +ATF_TC_WITH_CLEANUP(cross_jail_dirfd); +ATF_TC_HEAD(cross_jail_dirfd, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} +ATF_TC_BODY(cross_jail_dirfd, tc) +{ + int error, sock[2], jid1, jid2, status; + pid_t pid1, pid2; + + domainsocketpair(sock); + + error = mkdir("./a", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./b", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./c", 0755); + ATF_REQUIRE(error == 0); + error = mkdir("./a/c", 0755); + ATF_REQUIRE(error == 0); + + jid1 = jail_setv(JAIL_CREATE, + "name", "passfd_test_cross_jail_dirfd1", + "path", "./a", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid1 >= 0, "jail_setv: %s", jail_errmsg); + + jid2 = jail_setv(JAIL_CREATE, + "name", "passfd_test_cross_jail_dirfd2", + "path", "./b", + "persist", NULL, + NULL); + ATF_REQUIRE_MSG(jid2 >= 0, "jail_setv: %s", jail_errmsg); + + pid1 = fork(); + ATF_REQUIRE(pid1 >= 0); + if (pid1 == 0) { + ssize_t len; + int dfd, error; + char ch; + + error = jail_attach(jid1); + if (error != 0) + err(1, "jail_attach"); + + dfd = open(".", O_RDONLY | O_DIRECTORY); + if (dfd < 0) + err(1, "open(\".\") in jail %d", jid1); + + ch = 0; + len = sendfd_payload(sock[0], dfd, &ch, sizeof(ch)); + if (len == -1) + err(1, "sendmsg"); + + _exit(0); + } + + pid2 = fork(); + ATF_REQUIRE(pid2 >= 0); + if (pid2 == 0) { + ssize_t len; + int dfd, dfd2, error, fd; + char ch; + + error = jail_attach(jid2); + if (error != 0) + err(1, "jail_attach"); + + /* Get a directory from outside the jail root. */ + len = recvfd_payload(sock[1], &dfd, &ch, sizeof(ch), + CMSG_SPACE(sizeof(int)), 0); + if (len == -1) + err(1, "recvmsg"); + + if ((fcntl(dfd, F_GETFD) & FD_RESOLVE_BENEATH) == 0) + errx(1, "dfd does not have FD_RESOLVE_BENEATH set"); + + /* Make sure we can't chdir. */ + error = fchdir(dfd); + if (error == 0) + errx(1, "fchdir succeeded"); + if (errno != ENOTCAPABLE) + err(1, "fchdir"); + + /* Make sure a dotdot access fails. */ + fd = openat(dfd, "../c", O_RDONLY | O_DIRECTORY); + if (fd >= 0) + errx(1, "openat(\"../c\") succeeded"); + if (errno != ENOTCAPABLE) + err(1, "openat"); + + /* Accesses within the sender's jail root are ok. */ + fd = openat(dfd, "c", O_RDONLY | O_DIRECTORY); + if (fd < 0) + err(1, "openat(\"c\")"); + + dfd2 = openat(dfd, "", O_EMPTY_PATH | O_RDONLY | O_DIRECTORY); + if (dfd2 < 0) + err(1, "openat(\"\")"); + if ((fcntl(dfd2, F_GETFD) & FD_RESOLVE_BENEATH) == 0) + errx(1, "dfd2 does not have FD_RESOLVE_BENEATH set"); + + _exit(0); + } + + error = waitpid(pid1, &status, 0); + ATF_REQUIRE(error != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 0); + error = waitpid(pid2, &status, 0); + ATF_REQUIRE(error != -1); + ATF_REQUIRE(WIFEXITED(status)); + ATF_REQUIRE(WEXITSTATUS(status) == 0); + + closesocketpair(sock); +} +ATF_TC_CLEANUP(cross_jail_dirfd, tc) +{ + int jid; + + jid = jail_getid("passfd_test_cross_jail_dirfd1"); + if (jid >= 0 && jail_remove(jid) != 0) + err(1, "jail_remove"); + jid = jail_getid("passfd_test_cross_jail_dirfd2"); + if (jid >= 0 && jail_remove(jid) != 0) + err(1, "jail_remove"); +} + ATF_TP_ADD_TCS(tp) { @@ -997,6 +1175,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, send_and_shutdown); ATF_TP_ADD_TC(tp, send_a_lot); ATF_TP_ADD_TC(tp, send_overflow); + ATF_TP_ADD_TC(tp, peek); ATF_TP_ADD_TC(tp, two_files); ATF_TP_ADD_TC(tp, bundle); ATF_TP_ADD_TC(tp, bundle_cancel); @@ -1006,6 +1185,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, copyout_rights_error); ATF_TP_ADD_TC(tp, empty_rights_message); ATF_TP_ADD_TC(tp, control_creates_records); + ATF_TP_ADD_TC(tp, cross_jail_dirfd); return (atf_no_error()); } diff --git a/tests/sys/kern/unix_seqpacket_test.c b/tests/sys/kern/unix_seqpacket_test.c index dc5f89270a07..b9a6be015241 100644 --- a/tests/sys/kern/unix_seqpacket_test.c +++ b/tests/sys/kern/unix_seqpacket_test.c @@ -894,6 +894,38 @@ ATF_TC_BODY(shutdown_send_sigpipe, tc) close(s2); } +/* + * https://syzkaller.appspot.com/bug?id=ac94349a29f2efc40e9274239e4ca9b2c473a4e7 + */ +ATF_TC_WITHOUT_HEAD(shutdown_o_async); +ATF_TC_BODY(shutdown_o_async, tc) +{ + int sv[2]; + + do_socketpair(sv); + + ATF_CHECK_EQ(0, fcntl(sv[0], F_SETFL, O_ASYNC)); + ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR)); + close(sv[0]); + close(sv[1]); +} + +/* + * If peer had done SHUT_WR on their side, our recv(2) shouldn't block. + */ +ATF_TC_WITHOUT_HEAD(shutdown_recv); +ATF_TC_BODY(shutdown_recv, tc) +{ + char buf[10]; + int sv[2]; + + do_socketpair(sv); + ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR)); + ATF_CHECK_EQ(0, recv(sv[1], buf, sizeof(buf), 0)); + close(sv[0]); + close(sv[1]); +} + /* nonblocking send(2) and recv(2) a single short record */ ATF_TC_WITHOUT_HEAD(send_recv_nonblocking); ATF_TC_BODY(send_recv_nonblocking, tc) @@ -1207,7 +1239,7 @@ ATF_TC_BODY(random_eor_and_waitall, tc) for (u_int i = 0; i < RANDOM_TESTSIZE / (u_int )sizeof(long); i++) ((long *)params.sendbuf)[i] = nrand48(¶ms.seed[0]); - ATF_REQUIRE(sysctlbyname("net.local.stream.recvspace", + ATF_REQUIRE(sysctlbyname("net.local.seqpacket.recvspace", ¶ms.recvspace, &(size_t){sizeof(u_long)}, NULL, 0) != -1); ATF_REQUIRE((recvbuf = malloc(RANDOM_RECVSIZE * params.recvspace)) != NULL); @@ -1310,6 +1342,8 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, implied_connect); ATF_TP_ADD_TC(tp, shutdown_send); ATF_TP_ADD_TC(tp, shutdown_send_sigpipe); + ATF_TP_ADD_TC(tp, shutdown_o_async); + ATF_TP_ADD_TC(tp, shutdown_recv); ATF_TP_ADD_TC(tp, eagain_8k_8k); ATF_TP_ADD_TC(tp, eagain_8k_128k); ATF_TP_ADD_TC(tp, eagain_128k_8k); diff --git a/tests/sys/kern/unix_stream.c b/tests/sys/kern/unix_stream.c index d57cfad020fa..bb811f78f620 100644 --- a/tests/sys/kern/unix_stream.c +++ b/tests/sys/kern/unix_stream.c @@ -26,14 +26,18 @@ */ #include <sys/cdefs.h> -#include <errno.h> -#include <fcntl.h> -#include <pthread.h> -#include <signal.h> #include <sys/socket.h> +#include <sys/event.h> +#include <sys/select.h> +#include <sys/sysctl.h> #include <sys/un.h> - +#include <errno.h> +#include <fcntl.h> #include <stdio.h> +#include <stdlib.h> +#include <poll.h> +#include <pthread.h> +#include <pthread_np.h> #include <atf-c.h> @@ -49,6 +53,18 @@ do_socketpair(int *sv) ATF_REQUIRE(sv[0] != sv[1]); } +static u_long +getsendspace(void) +{ + u_long sendspace; + + ATF_REQUIRE_MSG(sysctlbyname("net.local.stream.sendspace", &sendspace, + &(size_t){sizeof(u_long)}, NULL, 0) != -1, + "sysctl net.local.stream.sendspace failed: %s", strerror(errno)); + + return (sendspace); +} + /* getpeereid(3) should work with stream sockets created via socketpair(2) */ ATF_TC_WITHOUT_HEAD(getpeereid); ATF_TC_BODY(getpeereid, tc) @@ -86,11 +102,386 @@ ATF_TC_BODY(send_0, tc) close(sv[1]); } +struct check_ctx; +typedef void check_func_t(struct check_ctx *); +struct check_ctx { + check_func_t *method; + int sv[2]; + bool timeout; + union { + enum { SELECT_RD, SELECT_WR } select_what; + short poll_events; + short kev_filter; + }; + int nfds; + union { + short poll_revents; + unsigned short kev_flags; + }; +}; + +static void +check_select(struct check_ctx *ctx) +{ + fd_set fds; + int nfds; + + FD_ZERO(&fds); + FD_SET(ctx->sv[0], &fds); + nfds = select(ctx->sv[0] + 1, + ctx->select_what == SELECT_RD ? &fds : NULL, + ctx->select_what == SELECT_WR ? &fds : NULL, + NULL, + ctx->timeout ? &(struct timeval){.tv_usec = 1000} : NULL); + ATF_REQUIRE_MSG(nfds == ctx->nfds, + "select() returns %d errno %d", nfds, errno); +} + +static void +check_poll(struct check_ctx *ctx) +{ + struct pollfd pfd[1]; + int nfds; + + pfd[0] = (struct pollfd){ + .fd = ctx->sv[0], + .events = ctx->poll_events, + }; + nfds = poll(pfd, 1, ctx->timeout ? 1 : INFTIM); + ATF_REQUIRE_MSG(nfds == ctx->nfds, + "poll() returns %d errno %d", nfds, errno); + ATF_REQUIRE((pfd[0].revents & ctx->poll_revents) == ctx->poll_revents); +} + +static void +check_kevent(struct check_ctx *ctx) +{ + struct kevent kev; + int nfds, kq; + + ATF_REQUIRE(kq = kqueue()); + EV_SET(&kev, ctx->sv[0], ctx->kev_filter, EV_ADD, 0, 0, NULL); + nfds = kevent(kq, &kev, 1, NULL, 0, NULL); + ATF_REQUIRE_MSG(nfds == 0, + "kevent() returns %d errno %d", nfds, errno); + nfds = kevent(kq, NULL, 0, &kev, 1, ctx->timeout ? + &(struct timespec){.tv_nsec = 1000000} : NULL); + ATF_REQUIRE_MSG(nfds == ctx->nfds, + "kevent() returns %d errno %d", nfds, errno); + ATF_REQUIRE(kev.ident == (uintptr_t)ctx->sv[0] && + kev.filter == ctx->kev_filter && + (kev.flags & ctx->kev_flags) == ctx->kev_flags); + close(kq); +} + +static void +full_socketpair(int *sv) +{ + void *buf; + u_long sendspace; + + sendspace = getsendspace(); + ATF_REQUIRE((buf = malloc(sendspace)) != NULL); + do_socketpair(sv); + ATF_REQUIRE(fcntl(sv[0], F_SETFL, O_NONBLOCK) != -1); + do {} while (send(sv[0], buf, sendspace, 0) == (ssize_t)sendspace); + ATF_REQUIRE(errno == EAGAIN); + ATF_REQUIRE(fcntl(sv[0], F_SETFL, 0) != -1); + free(buf); +} + +static void * +pthread_wrap(void *arg) +{ + struct check_ctx *ctx = arg; + + ctx->method(ctx); + + return (NULL); +} + +/* + * Launch a thread that would block in event mech and return it. + */ +static pthread_t +pthread_create_blocked(struct check_ctx *ctx) +{ + pthread_t thr; + + ctx->timeout = false; + ctx->nfds = 1; + ATF_REQUIRE(pthread_create(&thr, NULL, pthread_wrap, ctx) == 0); + + /* Sleep a bit to make sure that thread is put to sleep. */ + usleep(10000); + ATF_REQUIRE(pthread_peekjoin_np(thr, NULL) == EBUSY); + + return (thr); +} + +static void +full_writability_check(struct check_ctx *ctx) +{ + pthread_t thr; + void *buf; + u_long space; + + space = getsendspace() / 2; + ATF_REQUIRE((buf = malloc(space)) != NULL); + + /* First check with timeout, expecting 0 fds returned. */ + ctx->timeout = true; + ctx->nfds = 0; + ctx->method(ctx); + + thr = pthread_create_blocked(ctx); + + /* Read some data and re-check, the fd is expected to be returned. */ + ATF_REQUIRE(read(ctx->sv[1], buf, space) == (ssize_t)space); + + /* Now check that thread was successfully woken up and exited. */ + ATF_REQUIRE(pthread_join(thr, NULL) == 0); + + /* Extra check repeating what joined thread already did. */ + ctx->method(ctx); + + close(ctx->sv[0]); + close(ctx->sv[1]); + free(buf); +} + +/* + * Make sure that a full socket is not reported as writable by event APIs. + */ +ATF_TC_WITHOUT_HEAD(full_writability_select); +ATF_TC_BODY(full_writability_select, tc) +{ + struct check_ctx ctx = { + .method = check_select, + .select_what = SELECT_WR, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(full_writability_poll); +ATF_TC_BODY(full_writability_poll, tc) +{ + struct check_ctx ctx = { + .method = check_poll, + .poll_events = POLLOUT | POLLWRNORM, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(full_writability_kevent); +ATF_TC_BODY(full_writability_kevent, tc) +{ + struct check_ctx ctx = { + .method = check_kevent, + .kev_filter = EVFILT_WRITE, + }; + + full_socketpair(ctx.sv); + full_writability_check(&ctx); + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(connected_writability); +ATF_TC_BODY(connected_writability, tc) +{ + struct check_ctx ctx = { + .timeout = true, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(unconnected_writability); +ATF_TC_BODY(unconnected_writability, tc) +{ + struct check_ctx ctx = { + .timeout = true, + .nfds = 0, + }; + + ATF_REQUIRE((ctx.sv[0] = socket(PF_LOCAL, SOCK_STREAM, 0)) > 0); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); +} + +ATF_TC_WITHOUT_HEAD(peerclosed_writability); +ATF_TC_BODY(peerclosed_writability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + close(ctx.sv[1]); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + ctx.kev_filter = EVFILT_WRITE; + ctx.kev_flags = EV_EOF; + check_kevent(&ctx); + + close(ctx.sv[0]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_writability); +ATF_TC_BODY(peershutdown_writability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + + do_socketpair(ctx.sv); + shutdown(ctx.sv[1], SHUT_RD); + + ctx.select_what = SELECT_WR; + check_select(&ctx); + ctx.poll_events = POLLOUT | POLLWRNORM; + check_poll(&ctx); + /* + * XXXGL: historically unix(4) sockets were not reporting peer's + * shutdown(SHUT_RD) as our EV_EOF. The kevent(2) manual page says + * "filter will set EV_EOF when the reader disconnects", which is hard + * to interpret unambigously. For now leave the historic behavior, + * but we may want to change that in uipc_usrreq.c:uipc_filt_sowrite(), + * and then this test will also expect EV_EOF in returned flags. + */ + ctx.kev_filter = EVFILT_WRITE; + check_kevent(&ctx); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_readability); +ATF_TC_BODY(peershutdown_readability, tc) +{ + struct check_ctx ctx = { + .timeout = false, + .nfds = 1, + }; + ssize_t readsz; + char c; + + do_socketpair(ctx.sv); + shutdown(ctx.sv[1], SHUT_WR); + + /* + * The other side should flag as readable in select(2) to allow it to + * read(2) and observe EOF. Ensure that both poll(2) and select(2) + * are consistent here. + */ + ctx.select_what = SELECT_RD; + check_select(&ctx); + ctx.poll_events = POLLIN | POLLRDNORM; + check_poll(&ctx); + + /* + * Also check that read doesn't block. + */ + readsz = read(ctx.sv[0], &c, sizeof(c)); + ATF_REQUIRE_INTEQ(0, readsz); + + close(ctx.sv[0]); + close(ctx.sv[1]); +} + +static void +peershutdown_wakeup(struct check_ctx *ctx) +{ + pthread_t thr; + + ctx->timeout = false; + ctx->nfds = 1; + + do_socketpair(ctx->sv); + thr = pthread_create_blocked(ctx); + shutdown(ctx->sv[1], SHUT_WR); + ATF_REQUIRE(pthread_join(thr, NULL) == 0); + + close(ctx->sv[0]); + close(ctx->sv[1]); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_select); +ATF_TC_BODY(peershutdown_wakeup_select, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_select, + .select_what = SELECT_RD, + }); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_poll); +ATF_TC_BODY(peershutdown_wakeup_poll, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_poll, + .poll_events = POLLIN | POLLRDNORM | POLLRDHUP, + .poll_revents = POLLRDHUP, + }); +} + +ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_kevent); +ATF_TC_BODY(peershutdown_wakeup_kevent, tc) +{ + peershutdown_wakeup(&(struct check_ctx){ + .method = check_kevent, + .kev_filter = EVFILT_READ, + .kev_flags = EV_EOF, + }); +} ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, getpeereid); ATF_TP_ADD_TC(tp, send_0); + ATF_TP_ADD_TC(tp, connected_writability); + ATF_TP_ADD_TC(tp, unconnected_writability); + ATF_TP_ADD_TC(tp, full_writability_select); + ATF_TP_ADD_TC(tp, full_writability_poll); + ATF_TP_ADD_TC(tp, full_writability_kevent); + ATF_TP_ADD_TC(tp, peerclosed_writability); + ATF_TP_ADD_TC(tp, peershutdown_writability); + ATF_TP_ADD_TC(tp, peershutdown_readability); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_select); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_poll); + ATF_TP_ADD_TC(tp, peershutdown_wakeup_kevent); return atf_no_error(); } diff --git a/tests/sys/kqueue/Makefile.inc b/tests/sys/kqueue/Makefile.inc index d3b5cbd3a79b..01b5f23410c8 100644 --- a/tests/sys/kqueue/Makefile.inc +++ b/tests/sys/kqueue/Makefile.inc @@ -1,2 +1 @@ - .include "../Makefile.inc" diff --git a/tests/sys/kqueue/libkqueue/Makefile b/tests/sys/kqueue/libkqueue/Makefile index 797ff98f87a3..31eee2bd7c40 100644 --- a/tests/sys/kqueue/libkqueue/Makefile +++ b/tests/sys/kqueue/libkqueue/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/kqueue/libkqueue BINDIR= ${TESTSDIR} diff --git a/tests/sys/kqueue/libkqueue/timer.c b/tests/sys/kqueue/libkqueue/timer.c index 523dedc7c800..5116aea98b83 100644 --- a/tests/sys/kqueue/libkqueue/timer.c +++ b/tests/sys/kqueue/libkqueue/timer.c @@ -199,7 +199,7 @@ test_periodic_modify(void) kevent_cmp(&kev, kevent_get(kqfd)); /* Check if the event occurs again */ - EV_SET(&kev, vnode_fd, EVFILT_TIMER, EV_ADD, 0, 500, NULL); + EV_SET(&kev, vnode_fd, EVFILT_TIMER, EV_ADD, 0, 495, NULL); if (kevent(kqfd, &kev, 1, NULL, 0, NULL) < 0) err(1, "%s", test_id); diff --git a/tests/sys/mac/Makefile b/tests/sys/mac/Makefile index 4c38ce1d6c7f..3447d00122f5 100644 --- a/tests/sys/mac/Makefile +++ b/tests/sys/mac/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/mac TESTS_SUBDIRS+= bsdextended diff --git a/tests/sys/mac/Makefile.inc b/tests/sys/mac/Makefile.inc index d3b5cbd3a79b..01b5f23410c8 100644 --- a/tests/sys/mac/Makefile.inc +++ b/tests/sys/mac/Makefile.inc @@ -1,2 +1 @@ - .include "../Makefile.inc" diff --git a/tests/sys/mac/bsdextended/Makefile b/tests/sys/mac/bsdextended/Makefile index f5b07db386c2..69cd27c0e321 100644 --- a/tests/sys/mac/bsdextended/Makefile +++ b/tests/sys/mac/bsdextended/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/mac/bsdextended ATF_TESTS_SH+= matches_test diff --git a/tests/sys/mac/ipacl/Makefile b/tests/sys/mac/ipacl/Makefile index 4a5dfaa015c3..e083f6c1a69c 100644 --- a/tests/sys/mac/ipacl/Makefile +++ b/tests/sys/mac/ipacl/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/mac/ipacl diff --git a/tests/sys/mac/portacl/Makefile b/tests/sys/mac/portacl/Makefile index 47993fe54c54..c9fb6bbaae3e 100644 --- a/tests/sys/mac/portacl/Makefile +++ b/tests/sys/mac/portacl/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/mac/portacl diff --git a/tests/sys/mqueue/Makefile b/tests/sys/mqueue/Makefile index 05b5fc97c244..743c82ecc954 100644 --- a/tests/sys/mqueue/Makefile +++ b/tests/sys/mqueue/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/mqueue ATF_TESTS_SH= mqueue_test diff --git a/tests/sys/net/Makefile b/tests/sys/net/Makefile index 95ab86156a0a..bc8f9c5e9c80 100644 --- a/tests/sys/net/Makefile +++ b/tests/sys/net/Makefile @@ -7,6 +7,8 @@ ATF_TESTS_C+= if_epair ATF_TESTS_SH+= if_epair_test ATF_TESTS_SH+= if_bridge_test TEST_METADATA.if_bridge_test+= required_programs="python" +TEST_METADATA.if_bridge_test+= execenv="jail" +TEST_METADATA.if_bridge_test+= execenv_jail_params="vnet allow.raw_sockets" ATF_TESTS_SH+= if_clone_test ATF_TESTS_SH+= if_gif ATF_TESTS_SH+= if_lagg_test @@ -15,6 +17,7 @@ ATF_TESTS_SH+= if_tun_test ATF_TESTS_SH+= if_vlan ATF_TESTS_SH+= if_wg +TESTS_SUBDIRS+= bpf TESTS_SUBDIRS+= if_ovpn TESTS_SUBDIRS+= routing diff --git a/tests/sys/net/bpf/Makefile b/tests/sys/net/bpf/Makefile new file mode 100644 index 000000000000..9c8a25b15d16 --- /dev/null +++ b/tests/sys/net/bpf/Makefile @@ -0,0 +1,15 @@ +.include <src.opts.mk> + +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/net/bpf +BINDIR= ${TESTSDIR} + +LIBADD+= nv + +PROGS= bpf_multi_read +LIBADD.bpf_multi_read+= pcap + +ATF_TESTS_SH= bpf + +.include <bsd.test.mk> diff --git a/tests/sys/net/bpf/bpf.sh b/tests/sys/net/bpf/bpf.sh new file mode 100644 index 000000000000..2830c4862de9 --- /dev/null +++ b/tests/sys/net/bpf/bpf.sh @@ -0,0 +1,67 @@ +## +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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. + +. $(atf_get_srcdir)/../../common/vnet.subr + +atf_test_case "multi_read" "cleanup" +multi_read_head() +{ + atf_set descr 'Test multiple readers on /dev/bpf' + atf_set require.user root +} + +multi_read_body() +{ + vnet_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet 192.0.2.2/24 up + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + + # Start a multi-thread (or multi-process) read on bpf + $(atf_get_srcdir)/bpf_multi_read ${epair}a & + + # Generate traffic + ping -f 192.0.2.2 >/dev/null 2>&1 & + + # Now let this run for 10 seconds + sleep 10 +} + +multi_read_cleanup() +{ + vnet_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "multi_read" +} diff --git a/tests/sys/net/bpf/bpf_multi_read.c b/tests/sys/net/bpf/bpf_multi_read.c new file mode 100644 index 000000000000..3a8edd76d623 --- /dev/null +++ b/tests/sys/net/bpf/bpf_multi_read.c @@ -0,0 +1,76 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 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. + * + */ + +#include <err.h> +#include <stdio.h> +#include <pcap.h> +#include <unistd.h> + +static void +callback(u_char *arg __unused, const struct pcap_pkthdr *hdr __unused, + const unsigned char *bytes __unused) +{ +} + +int +main(int argc, const char **argv) +{ + pcap_t *pcap; + const char *interface; + char errbuf[PCAP_ERRBUF_SIZE] = { 0 }; + int ret; + + if (argc != 2) + err(1, "Usage: %s <interface>\n", argv[0]); + + interface = argv[1]; + + pcap = pcap_create(interface, errbuf); + if (! pcap) + perror("Failed to pcap interface"); + + ret = pcap_set_snaplen(pcap, 86); + if (ret != 0) + perror("Failed to set snaplen"); + + ret = pcap_set_timeout(pcap, 100); + if (ret != 0) + perror("Failed to set timeout"); + + ret = pcap_activate(pcap); + if (ret != 0) + perror("Failed to activate"); + + /* So we have two readers on one /dev/bpf fd */ + fork(); + + printf("Interface open\n"); + pcap_loop(pcap, 0, callback, NULL); + + return (0); +} diff --git a/tests/sys/net/if_bridge_test.sh b/tests/sys/net/if_bridge_test.sh index 44370a905223..cc0b212aebd2 100755 --- a/tests/sys/net/if_bridge_test.sh +++ b/tests/sys/net/if_bridge_test.sh @@ -39,6 +39,7 @@ bridge_transmit_ipv4_unicast_head() bridge_transmit_ipv4_unicast_body() { vnet_init + vnet_init_bridge epair_alcatraz=$(vnet_mkepair) epair_singsing=$(vnet_mkepair) @@ -76,6 +77,7 @@ stp_head() stp_body() { vnet_init + vnet_init_bridge epair_one=$(vnet_mkepair) epair_two=$(vnet_mkepair) @@ -142,6 +144,7 @@ stp_vlan_head() stp_vlan_body() { vnet_init + vnet_init_bridge epair_one=$(vnet_mkepair) epair_two=$(vnet_mkepair) @@ -216,6 +219,7 @@ static_head() static_body() { vnet_init + vnet_init_bridge epair=$(vnet_mkepair) bridge=$(vnet_mkbridge) @@ -267,12 +271,13 @@ span_head() { atf_set descr 'Bridge span test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } span_body() { vnet_init + vnet_init_bridge epair=$(vnet_mkepair) epair_span=$(vnet_mkepair) @@ -326,6 +331,7 @@ delete_with_members_head() delete_with_members_body() { vnet_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair=$(vnet_mkepair) @@ -352,6 +358,7 @@ mac_conflict_head() mac_conflict_body() { vnet_init + vnet_init_bridge epair=$(vnet_mkepair) @@ -390,6 +397,7 @@ inherit_mac_head() inherit_mac_body() { vnet_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair=$(vnet_mkepair) @@ -412,12 +420,13 @@ stp_validation_head() { atf_set descr 'Check STP validation' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } stp_validation_body() { vnet_init + vnet_init_bridge epair_one=$(vnet_mkepair) epair_two=$(vnet_mkepair) @@ -461,6 +470,7 @@ gif_head() gif_body() { vnet_init + vnet_init_bridge epair=$(vnet_mkepair) @@ -545,6 +555,7 @@ check_mtu() mtu_body() { vnet_init + vnet_init_bridge epair=$(vnet_mkepair) gif=$(ifconfig gif create) @@ -606,6 +617,7 @@ vlan_head() vlan_body() { vnet_init + vnet_init_bridge vid=1 @@ -673,6 +685,7 @@ many_bridge_members_head() many_bridge_members_body() { vnet_init + vnet_init_bridge bridge=$(vnet_mkbridge) ifcount=256 @@ -690,6 +703,524 @@ many_bridge_members_cleanup() vnet_cleanup } +atf_test_case "member_ifaddrs_enabled" "cleanup" +member_ifaddrs_enabled_head() +{ + atf_set descr 'bridge with member_ifaddrs=1' + atf_set require.user root +} + +member_ifaddrs_enabled_body() +{ + vnet_init + vnet_init_bridge + + ep=$(vnet_mkepair) + ifconfig ${ep}a inet 192.0.2.1/24 up + + vnet_mkjail one ${ep}b + jexec one sysctl net.link.bridge.member_ifaddrs=1 + jexec one ifconfig ${ep}b inet 192.0.2.2/24 up + jexec one ifconfig bridge0 create addm ${ep}b + + atf_check -s exit:0 -o ignore ping -c3 -t1 192.0.2.2 +} + +member_ifaddrs_enabled_cleanup() +{ + vnet_cleanup +} + +atf_test_case "member_ifaddrs_disabled" "cleanup" +member_ifaddrs_disabled_head() +{ + atf_set descr 'bridge with member_ifaddrs=0' + atf_set require.user root +} + +member_ifaddrs_disabled_body() +{ + vnet_init + vnet_init_bridge + + vnet_mkjail one + jexec one sysctl net.link.bridge.member_ifaddrs=0 + + bridge=$(jexec one ifconfig bridge create) + + # adding an interface with an IPv4 address + ep=$(jexec one ifconfig epair create) + jexec one ifconfig ${ep} 192.0.2.1/32 + atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep} + + # adding an interface with an IPv6 address + ep=$(jexec one ifconfig epair create) + jexec one ifconfig ${ep} inet6 2001:db8::1/128 + atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep} + + # adding an interface with an IPv6 link-local address + ep=$(jexec one ifconfig epair create) + jexec one ifconfig ${ep} inet6 -ifdisabled auto_linklocal up + atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep} + + # adding an IPv4 address to a member + ep=$(jexec one ifconfig epair create) + jexec one ifconfig ${bridge} addm ${ep} + atf_check -s exit:1 -e ignore jexec one ifconfig ${ep} inet 192.0.2.2/32 + + # adding an IPv6 address to a member + ep=$(jexec one ifconfig epair create) + jexec one ifconfig ${bridge} addm ${ep} + atf_check -s exit:1 -e ignore jexec one ifconfig ${ep} inet6 2001:db8::1/128 +} + +member_ifaddrs_disabled_cleanup() +{ + vnet_cleanup +} + +# +# Test kern/287150: when member_ifaddrs=0, and a physical interface which is in +# a bridge also has a vlan(4) on it, tagged packets are not correctly passed to +# vlan(4). +atf_test_case "member_ifaddrs_vlan" "cleanup" +member_ifaddrs_vlan_head() +{ + atf_set descr 'kern/287150: vlan and bridge on the same interface' + atf_set require.user root +} + +member_ifaddrs_vlan_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + # The first jail has an epair with an IP address on vlan 20. + vnet_mkjail one ${epone}a + atf_check -s exit:0 jexec one ifconfig ${epone}a up + atf_check -s exit:0 jexec one \ + ifconfig ${epone}a.20 create inet 192.0.2.1/24 up + + # The second jail has an epair with an IP address on vlan 20, + # which is also in a bridge. + vnet_mkjail two ${epone}b + + jexec two ifconfig + atf_check -s exit:0 -o save:bridge jexec two ifconfig bridge create + bridge=$(cat bridge) + atf_check -s exit:0 jexec two ifconfig ${bridge} addm ${epone}b up + + atf_check -s exit:0 -o ignore jexec two \ + sysctl net.link.bridge.member_ifaddrs=0 + atf_check -s exit:0 jexec two ifconfig ${epone}b up + atf_check -s exit:0 jexec two \ + ifconfig ${epone}b.20 create inet 192.0.2.2/24 up + + # Make sure the two jails can communicate over the vlan. + atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +member_ifaddrs_vlan_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_pvid" "cleanup" +vlan_pvid_head() +{ + atf_set descr 'bridge with two ports with pvid set' + atf_set require.user root +} + +vlan_pvid_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + jexec one ifconfig ${epone}b 192.0.2.1/24 up + jexec two ifconfig ${eptwo}b 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20 + ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20 + + # With VLAN filtering enabled, traffic should be passed. + atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 + + # Removed the untagged VLAN on one port; traffic should not be passed. + ifconfig ${bridge} -untagged ${epone}a + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_pvid_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_pvid_filtered" "cleanup" +vlan_pvid_filtered_head() +{ + atf_set descr 'bridge with two ports with different pvids' + atf_set require.user root +} + +vlan_pvid_filtered_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + jexec one ifconfig ${epone}b 192.0.2.1/24 up + jexec two ifconfig ${eptwo}b 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20 + ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 30 + + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_pvid_filtered_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_pvid_tagged" "cleanup" +vlan_pvid_tagged_head() +{ + atf_set descr 'bridge pvid with tagged frames for pvid' + atf_set require.user root +} + +vlan_pvid_tagged_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + # Create two tagged interfaces on the appropriate VLANs + jexec one ifconfig ${epone}b up + jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up + jexec two ifconfig ${eptwo}b up + jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20 + ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20 + + # Tagged frames should not be passed. + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_pvid_tagged_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_pvid_1q" "cleanup" +vlan_pvid_1q_head() +{ + atf_set descr '802.1q tag addition and removal' + atf_set require.user root +} + +vlan_pvid_1q_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + # Set up one jail with an access port, and the other with a trunk port. + # This forces the bridge to add and remove .1q tags to bridge the + # traffic. + + jexec one ifconfig ${epone}b 192.0.2.1/24 up + jexec two ifconfig ${eptwo}b up + jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20 + ifconfig ${bridge} addm ${eptwo}a + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + + atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_pvid_1q_cleanup() +{ + vnet_cleanup +} + +# +# Test vlan filtering. +# +atf_test_case "vlan_filtering" "cleanup" +vlan_filtering_head() +{ + atf_set descr 'tagged traffic with filtering' + atf_set require.user root +} + +vlan_filtering_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + jexec one ifconfig ${epone}b up + jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up + jexec two ifconfig ${eptwo}b up + jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a vlanfilter ${epone}a + ifconfig ${bridge} addm ${eptwo}a vlanfilter ${eptwo}a + + # Right now there are no VLANs on the access list, so everything + # should be blocked. + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 + + # Set the untagged vlan on both ports to 20 and make sure traffic is + # still blocked. We intentionally do not pass tagged traffic for the + # untagged vlan. + atf_check -s exit:0 ifconfig ${bridge} untagged ${epone}a 20 + atf_check -s exit:0 ifconfig ${bridge} untagged ${eptwo}a 20 + + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 + + atf_check -s exit:0 ifconfig ${bridge} -untagged ${epone}a + atf_check -s exit:0 ifconfig ${bridge} -untagged ${eptwo}a + + # Add VLANs 10-30 to the access list; now access should be allowed. + ifconfig ${bridge} +tagged ${epone}a 10-30 + ifconfig ${bridge} +tagged ${eptwo}a 10-30 + atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 + + # Remove vlan 20 from the access list, now access should be blocked + # again. + ifconfig ${bridge} -tagged ${epone}a 20 + ifconfig ${bridge} -tagged ${eptwo}a 20 + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_filtering_cleanup() +{ + vnet_cleanup +} + +# +# Test the ifconfig 'tagged' option. +# +atf_test_case "vlan_ifconfig_tagged" "cleanup" +vlan_ifconfig_tagged_head() +{ + atf_set descr 'test the ifconfig tagged option' + atf_set require.user root +} + +vlan_ifconfig_tagged_body() +{ + vnet_init + vnet_init_bridge + + ep=$(vnet_mkepair) + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} addm ${ep}a vlanfilter ${ep}a up + ifconfig ${ep}a up + + # To start with, no vlans should be configured. + atf_check -s exit:0 -o not-match:"tagged" ifconfig ${bridge} + + # Add vlans 100-149. + atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a 100-149 + atf_check -s exit:0 -o match:"tagged 100-149" ifconfig ${bridge} + + # Replace the vlan list with 139-199. + atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a 139-199 + atf_check -s exit:0 -o match:"tagged 139-199" ifconfig ${bridge} + + # Add vlans 100-170. + atf_check -s exit:0 ifconfig ${bridge} +tagged ${ep}a 100-170 + atf_check -s exit:0 -o match:"tagged 100-199" ifconfig ${bridge} + + # Remove vlans 104, 105, and 150-159 + atf_check -s exit:0 ifconfig ${bridge} -tagged ${ep}a 104,105,150-159 + atf_check -s exit:0 -o match:"tagged 100-103,106-149,160-199" \ + ifconfig ${bridge} + + # Remove the entire vlan list. + atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a none + atf_check -s exit:0 -o not-match:"tagged" ifconfig ${bridge} + + # Test some invalid vlans sets. + for bad_vlan in -1 0 4096 4097 foo 0-10 4000-5000 foo-40 40-foo; do + atf_check -s exit:1 -e ignore \ + ifconfig ${bridge} tagged "$bad_vlan" + done +} + +vlan_ifconfig_tagged_cleanup() +{ + vnet_cleanup +} + +# +# Test a vlan(4) "SVI" interface on top of a bridge. +# +atf_test_case "vlan_svi" "cleanup" +vlan_svi_head() +{ + atf_set descr 'vlan bridge with an SVI' + atf_set require.user root +} + +vlan_svi_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + + jexec one ifconfig ${epone}b up + jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${bridge} addm ${epone}a tagged ${epone}a 20 + + svi=$(vnet_mkvlan) + ifconfig ${svi} vlan 20 vlandev ${bridge} + ifconfig ${svi} inet 192.0.2.2/24 up + + atf_check -s exit:0 -o ignore ping -c 3 -t 1 192.0.2.1 +} + +vlan_svi_cleanup() +{ + vnet_cleanup +} + +# +# Test QinQ (802.1ad). +# +atf_test_case "vlan_qinq" "cleanup" +vlan_qinq_head() +{ + atf_set descr 'vlan filtering with QinQ traffic' + atf_set require.user root +} + +vlan_qinq_body() +{ + vnet_init + vnet_init_bridge + + epone=$(vnet_mkepair) + eptwo=$(vnet_mkepair) + + vnet_mkjail one ${epone}b + vnet_mkjail two ${eptwo}b + + # Create a QinQ trunk between the two jails. The outer (provider) tag + # is 5, and the inner tag is 10. + + jexec one ifconfig ${epone}b up + jexec one ifconfig ${epone}b.5 create vlanproto 802.1ad up + jexec one ifconfig ${epone}b.5.10 create inet 192.0.2.1/24 up + + jexec two ifconfig ${eptwo}b up + jexec two ifconfig ${eptwo}b.5 create vlanproto 802.1ad up + jexec two ifconfig ${eptwo}b.5.10 create inet 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + ifconfig ${bridge} up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a vlanfilter ${epone}a + ifconfig ${bridge} addm ${eptwo}a vlanfilter ${eptwo}a + + # Right now there are no VLANs on the access list, so everything + # should be blocked. + atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 + + # Add the provider tag to the access list; now traffic should be passed. + ifconfig ${bridge} +tagged ${epone}a 5 + ifconfig ${bridge} +tagged ${eptwo}a 5 + atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2 + atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1 +} + +vlan_qinq_cleanup() +{ + vnet_cleanup +} + atf_init_test_cases() { atf_add_test_case "bridge_transmit_ipv4_unicast" @@ -705,4 +1236,15 @@ atf_init_test_cases() atf_add_test_case "mtu" atf_add_test_case "vlan" atf_add_test_case "many_bridge_members" + atf_add_test_case "member_ifaddrs_enabled" + atf_add_test_case "member_ifaddrs_disabled" + atf_add_test_case "member_ifaddrs_vlan" + atf_add_test_case "vlan_pvid" + atf_add_test_case "vlan_pvid_1q" + atf_add_test_case "vlan_pvid_filtered" + atf_add_test_case "vlan_pvid_tagged" + atf_add_test_case "vlan_filtering" + atf_add_test_case "vlan_ifconfig_tagged" + atf_add_test_case "vlan_svi" + atf_add_test_case "vlan_qinq" } diff --git a/tests/sys/net/if_lagg_test.sh b/tests/sys/net/if_lagg_test.sh index 6b99aaedfbbf..e2b998599991 100755 --- a/tests/sys/net/if_lagg_test.sh +++ b/tests/sys/net/if_lagg_test.sh @@ -83,10 +83,6 @@ status_stress_body() { local TAP0 TAP1 LAGG MAC - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "Skipping this test because it panics the machine fairly often" - fi - # Configure the lagg interface to use an RFC5737 nonrouteable addresses ADDR="192.0.2.2" MASK="24" @@ -142,8 +138,6 @@ create_destroy_stress_body() { local TAP0 TAP1 LAGG MAC - atf_skip "Skipping this test because it easily panics the machine" - TAP0=`get_tap` TAP1=`get_tap` TAP2=`get_tap` @@ -196,10 +190,6 @@ lacp_linkstate_destroy_stress_head() } lacp_linkstate_destroy_stress_body() { - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/244168" - fi - local TAP0 TAP1 LAGG MAC SRCDIR # Configure the lagg interface to use an RFC5737 nonrouteable addresses @@ -261,8 +251,6 @@ up_destroy_stress_body() { local TAP0 TAP1 LAGG MAC SRCDIR - atf_skip "Skipping this test because it panics the machine fairly often" - # Configure the lagg interface to use an RFC5737 nonrouteable addresses ADDR="192.0.2.2" MASK="24" @@ -358,7 +346,6 @@ updown_body() { local TAP0 TAP1 LAGG MAC - atf_expect_fail "PR 226144 Upping a lagg interrface should automatically up its children" # Configure the lagg interface to use an RFC5737 nonrouteable addresses ADDR="192.0.2.2" MASK="24" diff --git a/tests/sys/net/if_ovpn/Makefile b/tests/sys/net/if_ovpn/Makefile index 823ad06e76ee..85746226e122 100644 --- a/tests/sys/net/if_ovpn/Makefile +++ b/tests/sys/net/if_ovpn/Makefile @@ -6,8 +6,8 @@ TESTSDIR= ${TESTSBASE}/sys/net/if_ovpn .if ${MK_PF} != "no" ATF_TESTS_SH+= if_ovpn -# Tests reuse jail names and so cannot run in parallel. -TEST_METADATA.if_ovpn+= is_exclusive=true +TEST_METADATA.if_ovpn+= execenv="jail" +TEST_METADATA.if_ovpn+= execenv_jail_params="vnet allow.raw_sockets" .endif ATF_TESTS_C+= if_ovpn_c diff --git a/tests/sys/net/if_ovpn/if_ovpn.sh b/tests/sys/net/if_ovpn/if_ovpn.sh index 0ec2563cf355..2138e0f666ec 100644 --- a/tests/sys/net/if_ovpn/if_ovpn.sh +++ b/tests/sys/net/if_ovpn/if_ovpn.sh @@ -95,6 +95,10 @@ atf_test_case "4in4" "cleanup" echo 'foo' | jexec b nc -u -w 2 192.0.2.1 1194 atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1 + + # Test routing loop protection + jexec b route add 192.0.2.1 198.51.100.1 + atf_check -s exit:2 -o ignore jexec b ping -t 1 -c 1 198.51.100.1 } 4in4_cleanup() @@ -102,6 +106,86 @@ atf_test_case "4in4" "cleanup" ovpn_cleanup } +atf_test_case "bz283426" "cleanup" +bz283426_head() +{ + atf_set descr 'FreeBSD Bugzilla 283426' + atf_set require.user root + atf_set require.progs openvpn python3 +} + +bz283426_body() +{ + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + jexec a ifconfig ${l}a 192.0.2.1/24 up + vnet_mkjail b ${l}b + jexec b ifconfig ${l}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore jexec a ping -c 1 192.0.2.2 + + ovpn_start a " + dev ovpn0 + dev-type tun + proto udp4 + + cipher AES-256-GCM + auth SHA256 + + bind 0.0.0.0:1194 + server 198.51.100.0 255.255.255.0 + ca $(atf_get_srcdir)/ca.crt + cert $(atf_get_srcdir)/server.crt + key $(atf_get_srcdir)/server.key + dh $(atf_get_srcdir)/dh.pem + + mode server + script-security 2 + auth-user-pass-verify /usr/bin/true via-env + topology subnet + + keepalive 100 600 + " + ovpn_start b " + dev tun0 + dev-type tun + + client + + remote 192.0.2.1 + auth-user-pass $(atf_get_srcdir)/user.pass + + ca $(atf_get_srcdir)/ca.crt + cert $(atf_get_srcdir)/client.crt + key $(atf_get_srcdir)/client.key + dh $(atf_get_srcdir)/dh.pem + + keepalive 100 600 + " + + # Give the tunnel time to come up + sleep 10 + + atf_check -s exit:0 -o ignore jexec b ping -c 1 198.51.100.1 + + # Send a broadcast packet in the outer link. + echo "import socket as sk +s = sk.socket(sk.AF_INET, sk.SOCK_DGRAM) +s.setsockopt(sk.SOL_SOCKET, sk.SO_BROADCAST, 1) +s.sendto(b'x' * 1000, ('192.0.2.255', 1194))" | jexec b python3 + + atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1 +} + +bz283426_cleanup() +{ + ovpn_cleanup +} + atf_test_case "4mapped" "cleanup" 4mapped_head() { @@ -404,6 +488,10 @@ atf_test_case "6in6" "cleanup" atf_check -s exit:0 -o ignore jexec b ping6 -c 3 2001:db8:1::1 atf_check -s exit:0 -o ignore jexec b ping6 -c 3 -z 16 2001:db8:1::1 + + # Test routing loop protection + jexec b route add -6 2001:db8::1 2001:db8:1::1 + atf_check -s exit:2 -o ignore jexec b ping6 -t 1 -c 3 2001:db8:1::1 } 6in6_cleanup() @@ -594,6 +682,7 @@ multi_client_head() multi_client_body() { ovpn_init + vnet_init_bridge bridge=$(vnet_mkbridge) srv=$(vnet_mkepair) @@ -806,6 +895,7 @@ ra_head() ra_body() { ovpn_init + vnet_init_bridge bridge=$(vnet_mkbridge) srv=$(vnet_mkepair) @@ -1038,9 +1128,31 @@ gcm_128_cleanup() ovpn_cleanup } +atf_test_case "destroy_unused" "cleanup" +destroy_unused_head() +{ + atf_set descr 'Destroy an if_ovpn interface before it is used' + atf_set require.user root +} + +destroy_unused_body() +{ + ovpn_init + + intf=$(ifconfig ovpn create) + atf_check -s exit:0 \ + ifconfig ${intf} destroy +} + +destroy_unused_cleanup() +{ + ovpn_cleanup +} + atf_init_test_cases() { atf_add_test_case "4in4" + atf_add_test_case "bz283426" atf_add_test_case "4mapped" atf_add_test_case "6in4" atf_add_test_case "6in6" @@ -1052,4 +1164,5 @@ atf_init_test_cases() atf_add_test_case "ra" atf_add_test_case "chacha" atf_add_test_case "gcm_128" + atf_add_test_case "destroy_unused" } diff --git a/tests/sys/net/if_vlan.sh b/tests/sys/net/if_vlan.sh index 675ed0090e8c..424eac705b94 100755 --- a/tests/sys/net/if_vlan.sh +++ b/tests/sys/net/if_vlan.sh @@ -22,8 +22,12 @@ basic_body() jexec alcatraz ifconfig ${epair_vlan}a up jexec alcatraz ifconfig ${vlan0} 10.0.0.1/24 up - vlan1=$(jexec singsing ifconfig vlan create vlandev ${epair_vlan}b \ - vlan 42) + vlan1=$(jexec singsing ifconfig vlan create) + + # Test associating the physical interface + atf_check -s exit:0 \ + jexec singsing ifconfig ${vlan1} vlandev ${epair_vlan}b vlan 42 + jexec singsing ifconfig ${epair_vlan}b up jexec singsing ifconfig ${vlan1} 10.0.0.2/24 up @@ -37,7 +41,7 @@ basic_body() # And change back # Test changing the vlan ID atf_check -s exit:0 \ - jexec singsing ifconfig ${vlan1} vlandev ${epair_vlan}b vlan 42 + jexec singsing ifconfig ${vlan1} vlan 42 vlandev ${epair_vlan}b atf_check -s exit:0 -o ignore jexec singsing ping -c 1 10.0.0.1 } @@ -253,7 +257,7 @@ bpf_pcp_head() { atf_set descr 'Set VLAN PCP through BPF' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } bpf_pcp_body() @@ -293,6 +297,42 @@ bpf_pcp_cleanup() vnet_cleanup } +atf_test_case "conflict_id" "cleanup" +conflict_id_head() +{ + atf_set descr 'Test conflicting VLAN IDs, PR #279195' + atf_set require.user root +} + +conflict_id_body() +{ + vnet_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}b + vlan_a=$(jexec alcatraz ifconfig vlan create) + vlan_b=$(jexec alcatraz ifconfig vlan create) + + jexec alcatraz ifconfig ${vlan_a} vlan 100 vlandev ${epair}b + jexec alcatraz ifconfig ${vlan_b} vlan 101 vlandev ${epair}b + + atf_check -s exit:1 -o ignore -e ignore \ + jexec alcatraz ifconfig ${vlan_a} vlan 101 + + atf_check -s exit:0 -o match:"vlan: 100" \ + jexec alcatraz ifconfig ${vlan_a} + + atf_check -s exit:0 -o ignore -e ignore \ + jexec alcatraz ifconfig ${vlan_a} vlan 100 +} + +conflict_id_cleanup() +{ + vnet_cleanup + +} + atf_init_test_cases() { atf_add_test_case "basic" @@ -302,4 +342,5 @@ atf_init_test_cases() atf_add_test_case "qinq_dot" atf_add_test_case "qinq_setflags" atf_add_test_case "bpf_pcp" + atf_add_test_case "conflict_id" } diff --git a/tests/sys/net/if_wg.sh b/tests/sys/net/if_wg.sh index b43b40f25018..1f51d86c8efa 100644 --- a/tests/sys/net/if_wg.sh +++ b/tests/sys/net/if_wg.sh @@ -34,6 +34,7 @@ wg_basic_head() { atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails' atf_set require.user root + atf_set require.kmods if_wg } wg_basic_body() @@ -41,8 +42,6 @@ wg_basic_body() local epair pri1 pri2 pub1 pub2 wg1 wg2 local endpoint1 endpoint2 tunnel1 tunnel2 - kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" - pri1=$(wg genkey) pri2=$(wg genkey) @@ -92,11 +91,90 @@ wg_basic_cleanup() vnet_cleanup } +atf_test_case "wg_basic_crossaf" "cleanup" +wg_basic_crossaf_head() +{ + atf_set descr 'Create a wg(4) tunnel and pass IPv4 traffic over an IPv6 nexthop' + atf_set require.user root +} + +wg_basic_crossaf_body() +{ + local epair pri1 pri2 pub1 pub2 wg1 wg2 + local endpoint1 endpoint2 tunnel1 tunnel2 + local testnet testlocal testremote + + kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" + + pri1=$(wg genkey) + pri2=$(wg genkey) + + endpoint1=192.168.2.1 + endpoint2=192.168.2.2 + tunnel1=2001:db8:1::1 + tunnel2=2001:db8:1::2 + + testnet=192.168.3.0/24 + testlocal=192.168.3.1 + testremote=192.168.3.2 + + epair=$(vnet_mkepair) + + vnet_init + + vnet_mkjail wgtest1 ${epair}a + vnet_mkjail wgtest2 ${epair}b + + jexec wgtest1 ifconfig ${epair}a ${endpoint1}/24 up + jexec wgtest2 ifconfig ${epair}b ${endpoint2}/24 up + + wg1=$(jexec wgtest1 ifconfig wg create) + echo "$pri1" | jexec wgtest1 wg set $wg1 listen-port 12345 \ + private-key /dev/stdin + pub1=$(jexec wgtest1 wg show $wg1 public-key) + wg2=$(jexec wgtest2 ifconfig wg create) + echo "$pri2" | jexec wgtest2 wg set $wg2 listen-port 12345 \ + private-key /dev/stdin + pub2=$(jexec wgtest2 wg show $wg2 public-key) + + atf_check -s exit:0 -o ignore \ + jexec wgtest1 wg set $wg1 peer "$pub2" \ + endpoint ${endpoint2}:12345 allowed-ips ${tunnel2}/128,${testnet} + atf_check -s exit:0 \ + jexec wgtest1 ifconfig $wg1 inet6 ${tunnel1}/64 up + + atf_check -s exit:0 -o ignore \ + jexec wgtest2 wg set $wg2 peer "$pub1" \ + endpoint ${endpoint1}:12345 allowed-ips ${tunnel1}/128,${testnet} + atf_check -s exit:0 \ + jexec wgtest2 ifconfig $wg2 inet6 ${tunnel2}/64 up + + atf_check -s exit:0 jexec wgtest1 ifconfig $wg1 inet ${testlocal}/32 + atf_check -s exit:0 jexec wgtest2 ifconfig $wg2 inet ${testremote}/32 + + # Generous timeout since the handshake takes some time. + atf_check -s exit:0 -o ignore jexec wgtest1 ping -c 1 -t 5 "$tunnel2" + + # Setup our IPv6 endpoint and routing + atf_check -s exit:0 -o ignore \ + jexec wgtest1 route add -inet ${testnet} -inet6 "$tunnel2" + atf_check -s exit:0 -o ignore \ + jexec wgtest2 route add -inet ${testnet} -inet6 "$tunnel1" + # Now ping an address on the other side + atf_check -s exit:0 -o ignore jexec wgtest1 ping -c 1 -t 3 ${testremote} +} + +wg_basic_crossaf_cleanup() +{ + vnet_cleanup +} + atf_test_case "wg_basic_netmap" "cleanup" wg_basic_netmap_head() { atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails with netmap' atf_set require.user root + atf_set require.kmods if_wg netmap } wg_basic_netmap_body() @@ -105,9 +183,6 @@ wg_basic_netmap_body() local endpoint1 endpoint2 tunnel1 tunnel2 tunnel3 tunnel4 local pid status - kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" - kldload -n netmap || atf_skip "This test requires netmap and could not load it" - pri1=$(wg genkey) pri2=$(wg genkey) @@ -190,6 +265,7 @@ wg_key_peerdev_shared_head() { atf_set descr 'Create a wg(4) interface with a shared pubkey between device and a peer' atf_set require.user root + atf_set require.kmods if_wg } wg_key_peerdev_shared_body() @@ -197,8 +273,6 @@ wg_key_peerdev_shared_body() local epair pri1 pub1 wg1 local endpoint1 tunnel1 - kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" - pri1=$(wg genkey) endpoint1=192.168.2.1 @@ -238,8 +312,6 @@ wg_key_peerdev_makeshared_body() local epair pri1 pub1 pri2 wg1 wg2 local endpoint1 tunnel1 - kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" - pri1=$(wg genkey) pri2=$(wg genkey) @@ -283,6 +355,7 @@ wg_vnet_parent_routing_head() { atf_set descr 'Create a wg(4) tunnel without epairs and pass traffic between jails' atf_set require.user root + atf_set require.kmods if_wg } wg_vnet_parent_routing_body() @@ -290,8 +363,6 @@ wg_vnet_parent_routing_body() local pri1 pri2 pub1 pub2 wg1 wg2 local tunnel1 tunnel2 - kldload -n if_wg - pri1=$(wg genkey) pri2=$(wg genkey) @@ -346,11 +417,217 @@ wg_vnet_parent_routing_cleanup() vnet_cleanup } +# The kernel should now allow removing a single allowed-ip without having to +# replace the whole list. We can't really test the atomicity of it all that +# easily, but we'll trust that it worked right if just that addr/mask is gone. +atf_test_case "wg_allowedip_incremental" "cleanup" +wg_allowedip_incremental_head() +{ + atf_set descr "Add/remove allowed-ips from a peer with the +/- incremental syntax" + atf_set require.user root +} + +wg_allowedip_incremental_body() +{ + local pri1 pri2 pub1 pub2 wg1 + local tunnel1 tunnel2 tunnel3 + + kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" + + pri1=$(wg genkey) + pri2=$(wg genkey) + pub2=$(echo "$pri2" | wg pubkey) + + tunnel1=169.254.0.1 + tunnel2=169.254.0.2 + tunnel3=169.254.0.3 + + vnet_mkjail wgtest1 + + wg1=$(jexec wgtest1 ifconfig wg create) + echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin + pub1=$(jexec wgtest1 wg show $wg1 public-key) + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "${tunnel1}/32,${tunnel2}/32" + + atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -q "${tunnel1}/32" wg.allowed + atf_check grep -q "${tunnel2}/32" wg.allowed + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "-${tunnel2}/32" + + atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -q "${tunnel1}/32" wg-2.allowed + atf_check -s not-exit:0 grep -q "${tunnel2}/32" wg-2.allowed + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "+${tunnel2}/32" + + atf_check -o save:wg-3.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -q "${tunnel1}/32" wg-3.allowed + atf_check grep -q "${tunnel2}/32" wg-3.allowed + + # Now attempt to add the address yet again to confirm that it's not + # harmful. + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "+${tunnel2}/32" + + atf_check -o save:wg-4.allowed -x \ + "jexec wgtest1 wg show $wg1 allowed-ips | cut -f2 | tr ' ' '\n'" + atf_check -o match:"2 wg-4.allowed$" wc -l wg-4.allowed + + # Finally, let's try removing an address that we never had at all and + # confirm that we still have our two addresses. + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "-${tunnel3}/32" + + atf_check -o save:wg-5.allowed -x \ + "jexec wgtest1 wg show $wg1 allowed-ips | cut -f2 | tr ' ' '\n'" + atf_check cmp -s wg-4.allowed wg-5.allowed +} + +wg_allowedip_incremental_cleanup() +{ + vnet_cleanup +} + +atf_test_case "wg_allowedip_incremental_inet6" "cleanup" +wg_allowedip_incremental_inet6_head() +{ + atf_set descr "Add/remove IPv6 allowed-ips from a peer with the +/- incremental syntax" + atf_set require.user root +} + +wg_allowedip_incremental_inet6_body() +{ + local pri1 pri2 pub1 pub2 wg1 + local tunnel1 tunnel2 + + kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" + + pri1=$(wg genkey) + pri2=$(wg genkey) + pub2=$(echo "$pri2" | wg pubkey) + + tunnel1=2001:db8:1::1 + tunnel2=2001:db8:1::2 + + vnet_mkjail wgtest1 + + wg1=$(jexec wgtest1 ifconfig wg create) + echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin + pub1=$(jexec wgtest1 wg show $wg1 public-key) + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "${tunnel1}/128" + atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -q "${tunnel1}/128" wg.allowed + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "+${tunnel2}/128" + atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -q "${tunnel1}/128" wg-2.allowed + atf_check grep -q "${tunnel2}/128" wg-2.allowed + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "-${tunnel1}/128" + atf_check -o save:wg-3.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check -s not-exit:0 grep -q "${tunnel1}/128" wg-3.allowed + atf_check grep -q "${tunnel2}/128" wg-3.allowed +} + +wg_allowedip_incremental_inet6_cleanup() +{ + vnet_cleanup +} + + +atf_test_case "wg_allowedip_incremental_stealing" "cleanup" +wg_allowedip_incremental_stealing_head() +{ + atf_set descr "Add/remove allowed-ips from a peer with the +/- incremental syntax to steal" + atf_set require.user root +} + +wg_allowedip_incremental_stealing_body() +{ + local pri1 pri2 pri3 pub1 pub2 pub3 wg1 + local regex2 regex3 + local tunnel1 tunnel2 + + kldload -n if_wg || atf_skip "This test requires if_wg and could not load it" + + pri1=$(wg genkey) + pri2=$(wg genkey) + pri3=$(wg genkey) + pub2=$(echo "$pri2" | wg pubkey) + pub3=$(echo "$pri3" | wg pubkey) + + regex2=$(echo "$pub2" | sed -e 's/[+]/[+]/g') + regex3=$(echo "$pub3" | sed -e 's/[+]/[+]/g') + + tunnel1=169.254.0.1 + tunnel2=169.254.0.2 + tunnel3=169.254.0.3 + + vnet_mkjail wgtest1 + + wg1=$(jexec wgtest1 ifconfig wg create) + echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin + pub1=$(jexec wgtest1 wg show $wg1 public-key) + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "${tunnel1}/32,${tunnel2}/32" + + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub3 \ + allowed-ips "${tunnel3}/32" + + # First, confirm that the negative syntax doesn't do anything because + # we have the wrong peer. + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "-${tunnel3}/32" + + atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips + atf_check grep -Eq "^${regex3}.+${tunnel3}/32" wg.allowed + + # Next, steal it with an incremental move and check that it moved. + atf_check -s exit:0 \ + jexec wgtest1 wg set $wg1 peer $pub2 \ + allowed-ips "+${tunnel3}/32" + + atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips + + atf_check grep -Eq "^${regex2}.+${tunnel3}/32" wg-2.allowed + atf_check grep -Evq "^${regex3}.+${tunnel3}/32" wg-2.allowed +} + +wg_allowedip_incremental_stealing_cleanup() +{ + vnet_cleanup +} + atf_init_test_cases() { atf_add_test_case "wg_basic" + atf_add_test_case "wg_basic_crossaf" atf_add_test_case "wg_basic_netmap" atf_add_test_case "wg_key_peerdev_shared" atf_add_test_case "wg_key_peerdev_makeshared" atf_add_test_case "wg_vnet_parent_routing" + atf_add_test_case "wg_allowedip_incremental" + atf_add_test_case "wg_allowedip_incremental_inet6" + atf_add_test_case "wg_allowedip_incremental_stealing" } diff --git a/tests/sys/net/routing/Makefile b/tests/sys/net/routing/Makefile index c98e4e2a2eaf..c725d23f15d1 100644 --- a/tests/sys/net/routing/Makefile +++ b/tests/sys/net/routing/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests WARNS?= 1 diff --git a/tests/sys/netgraph/Makefile b/tests/sys/netgraph/Makefile index 2240570ff46a..f0eb4928ec7f 100644 --- a/tests/sys/netgraph/Makefile +++ b/tests/sys/netgraph/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netgraph diff --git a/tests/sys/netgraph/ksocket.c b/tests/sys/netgraph/ksocket.c index e97b9b3f0691..a60c17bd337f 100644 --- a/tests/sys/netgraph/ksocket.c +++ b/tests/sys/netgraph/ksocket.c @@ -50,26 +50,28 @@ hellocheck(int wr, int rd) ATF_TC_WITHOUT_HEAD(udp_connect); ATF_TC_BODY(udp_connect, tc) { - struct sockaddr sa = { - .sa_family = AF_INET, - }; - socklen_t slen = sizeof(sa); - int ds, cs, us; - - ATF_REQUIRE((us = socket(PF_INET, SOCK_DGRAM, 0)) > 0); - ATF_REQUIRE(bind(us, &sa, sizeof(sa)) == 0); - ATF_REQUIRE(getsockname(us, &sa, &slen) == 0); - struct ngm_mkpeer mkp = { .type = NG_KSOCKET_NODE_TYPE, .ourhook = OURHOOK, .peerhook = "inet/dgram/udp", }; + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), + .sin_len = sizeof(sin), + }; + socklen_t slen = sizeof(sin); + int cs, ds, us; + + ATF_REQUIRE((us = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(bind(us, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(getsockname(us, (struct sockaddr *)&sin, &slen) == 0); + ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0); ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) >= 0); ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE, - NGM_KSOCKET_CONNECT, &sa, sizeof(sa)) >= 0); + NGM_KSOCKET_CONNECT, &sin, sizeof(sin)) >= 0); hellocheck(ds, us); } @@ -77,18 +79,19 @@ ATF_TC_BODY(udp_connect, tc) ATF_TC_WITHOUT_HEAD(udp_bind); ATF_TC_BODY(udp_bind, tc) { + struct ngm_mkpeer mkp = { + .type = NG_KSOCKET_NODE_TYPE, + .ourhook = OURHOOK, + .peerhook = "inet/dgram/udp", + }; struct sockaddr_in sin = { .sin_family = AF_INET, + .sin_addr.s_addr = htonl(INADDR_LOOPBACK), .sin_len = sizeof(sin), }; struct ng_mesg *rep; - int ds, cs, us; + int cs, ds, us; - struct ngm_mkpeer mkp = { - .type = NG_KSOCKET_NODE_TYPE, - .ourhook = OURHOOK, - .peerhook = "inet/dgram/udp", - }; ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0); ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) >= 0); @@ -106,10 +109,72 @@ ATF_TC_BODY(udp_bind, tc) hellocheck(us, ds); } +ATF_TC_WITHOUT_HEAD(udp6_connect); +ATF_TC_BODY(udp6_connect, tc) +{ + struct ngm_mkpeer mkp = { + .type = NG_KSOCKET_NODE_TYPE, + .ourhook = OURHOOK, + .peerhook = "inet6/dgram/udp6", + }; + struct sockaddr_in6 sin6 = { + .sin6_family = AF_INET6, + }; + socklen_t slen = sizeof(sin6); + int cs, ds, us; + + ATF_REQUIRE((us = socket(PF_INET6, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(bind(us, (struct sockaddr *)&sin6, sizeof(sin6)) == 0); + ATF_REQUIRE(getsockname(us, (struct sockaddr *)&sin6, &slen) == 0); + + ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0); + ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp, + sizeof(mkp)) >= 0); + ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE, + NGM_KSOCKET_CONNECT, &sin6, sizeof(sin6)) >= 0); + + hellocheck(ds, us); +} + + +ATF_TC_WITHOUT_HEAD(udp6_bind); +ATF_TC_BODY(udp6_bind, tc) +{ + struct ngm_mkpeer mkp = { + .type = NG_KSOCKET_NODE_TYPE, + .ourhook = OURHOOK, + .peerhook = "inet6/dgram/udp6", + }; + struct sockaddr_in6 sin6 = { + .sin6_family = AF_INET6, + .sin6_len = sizeof(sin6), + }; + struct ng_mesg *rep; + int cs, ds, us; + + ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0); + ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp, + sizeof(mkp)) >= 0); + ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE, + NGM_KSOCKET_BIND, &sin6, sizeof(sin6)) >= 0); + ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE, + NGM_KSOCKET_GETNAME, NULL, 0) >= 0); + ATF_REQUIRE(NgAllocRecvMsg(cs, &rep, NULL) == sizeof(struct ng_mesg) + + sizeof(struct sockaddr_in6)); + + ATF_REQUIRE((us = socket(PF_INET6, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(connect(us, (struct sockaddr *)rep->data, + sizeof(struct sockaddr_in6)) == 0); + + hellocheck(us, ds); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, udp_connect); ATF_TP_ADD_TC(tp, udp_bind); + ATF_TP_ADD_TC(tp, udp6_connect); + ATF_TP_ADD_TC(tp, udp6_bind); return (atf_no_error()); } diff --git a/tests/sys/netinet/Makefile b/tests/sys/netinet/Makefile index fb3281028aff..cc525bf24480 100644 --- a/tests/sys/netinet/Makefile +++ b/tests/sys/netinet/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netinet @@ -6,13 +5,16 @@ BINDIR= ${TESTSDIR} TESTS_SUBDIRS+= libalias -ATF_TESTS_C= ip_reass_test \ +ATF_TESTS_C= broadcast \ + fibs_multibind_test \ + ip_reass_test \ ip6_v4mapped_test \ so_reuseport_lb_test \ socket_afinet \ tcp_connect_port_test \ tcp_implied_connect \ tcp_md5_getsockopt \ + udp_bindings \ udp_io ATF_TESTS_SH= arp \ @@ -22,19 +24,31 @@ ATF_TESTS_SH= arp \ fibs_test \ forward \ lpm \ + multicast \ output \ redirect ATF_TESTS_PYTEST+= carp.py ATF_TESTS_PYTEST+= igmp.py -TEST_METADATA.divert+= required_programs="python" -TEST_METADATA.forward+= required_programs="python" +LIBADD.so_reuseport_lb_test= pthread +LIBADD.udp_bindings= pthread + +# Some of the arp tests look for log messages in the dmesg buffer, so run them +# serially to avoid problems with interleaved output. +TEST_METADATA.arp+= is_exclusive="true" +TEST_METADATA.divert+= required_programs="python" \ + execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.fibs_test+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.forward+= required_programs="python" \ + execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" TEST_METADATA.output+= required_programs="python" TEST_METADATA.redirect+= required_programs="python" -TEST_METADATA.tcp6_v4mapped_bind_test+= is_exclusive="true" -PROGS= udp_dontroute tcp_user_cookie +PROGS= udp_dontroute tcp_user_cookie sendto-IP_MULTICAST_IF ${PACKAGE}FILES+= redirect.py diff --git a/tests/sys/netinet/arp.sh b/tests/sys/netinet/arp.sh index 34946d42f250..df5dbc50ffa1 100755 --- a/tests/sys/netinet/arp.sh +++ b/tests/sys/netinet/arp.sh @@ -188,7 +188,9 @@ static_body() { ipa=198.51.100.1 ipb=198.51.100.2 + ipb_re=$(echo ${ipb} | sed 's/\./\\./g') max_age=$(sysctl -n net.link.ether.inet.max_age) + max_age="(${max_age}|$((${max_age} - 1)))" atf_check ifconfig -j ${jname}a ${epair0}a inet ${ipa}/24 eth="$(ifconfig -j ${jname}b ${epair0}b | @@ -197,8 +199,8 @@ static_body() { # Expected outputs permanent=\ "? (${ipb}) at 00:00:00:00:00:00 on ${epair0}a permanent [ethernet]\n" - temporary=\ -"? (${ipb}) at ${eth} on ${epair0}a expires in ${max_age} seconds [ethernet]\n" + temporary_re=\ +"\? \(${ipb_re}\) at ${eth} on ${epair0}a expires in ${max_age} seconds \[ethernet\]" deleted=\ "${ipb} (${ipb}) deleted\n" @@ -217,7 +219,7 @@ static_body() { # then check -S atf_check -o "inline:${deleted}" jexec ${jname}a arp -nd ${ipb} atf_check -o ignore jexec ${jname}b ping -c1 ${ipa} - atf_check -o "inline:${temporary}" jexec ${jname}a arp -n ${ipb} + atf_check -o "match:${temporary_re}" jexec ${jname}a arp -n ${ipb} # Note: this doesn't fail, tracked all the way down to FreeBSD 8 # atf_check -s not-exit:0 jexec ${jname}a arp -s ${ipb} 0:0:0:0:0:0 atf_check -o "inline:${deleted}" \ @@ -229,6 +231,32 @@ static_cleanup() { vnet_cleanup } +atf_test_case "garp" "cleanup" +garp_head() { + atf_set descr 'Basic gratuitous arp test' + atf_set require.user root +} + +garp_body() { + vnet_init + + j="v4t-garp" + + epair=$(vnet_mkepair) + + vnet_mkjail ${j} ${epair}a + atf_check -s exit:0 -o ignore \ + jexec ${j} sysctl net.link.ether.inet.garp_rexmit_count=3 + jexec ${j} ifconfig ${epair}a inet 192.0.2.1/24 up + + # Allow some time for the timer to actually fire + sleep 5 +} + +garp_cleanup() { + vnet_cleanup +} + atf_init_test_cases() { @@ -238,6 +266,7 @@ atf_init_test_cases() atf_add_test_case "pending_delete_if" atf_add_test_case "arp_lookup_host" atf_add_test_case "static" + atf_add_test_case "garp" } # end diff --git a/tests/sys/netinet/broadcast.c b/tests/sys/netinet/broadcast.c new file mode 100644 index 000000000000..e7850d513663 --- /dev/null +++ b/tests/sys/netinet/broadcast.c @@ -0,0 +1,196 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org> + * + * 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. + */ + +#include <sys/param.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <net/if.h> +#include <arpa/inet.h> +#include <errno.h> +#include <ifaddrs.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <atf-c.h> + +static const char buf[] = "Hello"; + +/* Create a UDP socket with SO_BROADCAST set. */ +static int +bcastsock(void) +{ + int s; + + ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(setsockopt(s, SOL_SOCKET, SO_BROADCAST, &(int){1}, + sizeof(int)) == 0); + return (s); +} + +/* Send on socket 's' with address 'to', confirm receive on 'r'. */ +static void +bcastecho(int s, struct sockaddr_in *to, int r) +{ + char rbuf[sizeof(buf)]; + + printf("Sending to %s\n", inet_ntoa(to->sin_addr)); + ATF_REQUIRE_MSG(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)to, + sizeof(*to)) == sizeof(buf), "sending of broadcast failed: %d", + errno); + ATF_REQUIRE(recv(r, rbuf, sizeof(rbuf), 0) == sizeof(rbuf)); + ATF_REQUIRE_MSG(memcmp(buf, rbuf, sizeof(buf)) == 0, + "failed to receive own broadcast"); +} + +/* Find a first broadcast capable interface and copy its broadcast address. */ +static void +firstbcast(struct in_addr *out) +{ + struct ifaddrs *ifa0, *ifa; + struct sockaddr_in sin; + + ATF_REQUIRE(getifaddrs(&ifa0) == 0); + for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) + if (ifa->ifa_addr->sa_family == AF_INET && + (ifa->ifa_flags & IFF_BROADCAST)) + break; + if (ifa == NULL) { + freeifaddrs(ifa0); + atf_tc_skip("No broadcast address found"); + } + memcpy(&sin, ifa->ifa_broadaddr, sizeof(struct sockaddr_in)); + *out = sin.sin_addr; + freeifaddrs(ifa0); +} + +/* Application sends to INADDR_BROADCAST, and this goes on the wire. */ +ATF_TC(INADDR_BROADCAST); +ATF_TC_HEAD(INADDR_BROADCAST, tc) +{ + atf_tc_set_md_var(tc, "require.config", "allow_network_access"); +} +ATF_TC_BODY(INADDR_BROADCAST, tc) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + }; + socklen_t slen = sizeof(sin); + int l, s; + + l = bcastsock(); + ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0); + sin.sin_addr.s_addr = htonl(INADDR_BROADCAST); + + s = bcastsock(); + bcastecho(s, &sin, l); + + close(s); + close(l); +} + +/* + * Application sends on broadcast address of an interface, INADDR_BROADCAST + * goes on the wire of the selected interface. + */ +ATF_TC_WITHOUT_HEAD(IP_ONESBCAST); +ATF_TC_BODY(IP_ONESBCAST, tc) +{ + struct ifaddrs *ifa0, *ifa; + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + }; + socklen_t slen = sizeof(sin); + int s, l; + in_port_t port; + bool skip = true; + + s = bcastsock(); + ATF_REQUIRE(setsockopt(s, IPPROTO_IP, IP_ONESBCAST, &(int){1}, + sizeof(int)) == 0); + + l = bcastsock(); + ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0); + port = sin.sin_port; + + ATF_REQUIRE(getifaddrs(&ifa0) == 0); + for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr->sa_family != AF_INET) + continue; + if (!(ifa->ifa_flags & IFF_BROADCAST)) + continue; + skip = false; + memcpy(&sin, ifa->ifa_broadaddr, sizeof(struct sockaddr_in)); + sin.sin_port = port; + bcastecho(s, &sin, l); + } + freeifaddrs(ifa0); + close(s); + close(l); + if (skip) + atf_tc_skip("No broadcast address found"); +} + +/* + * Application sends on broadcast address of an interface, and this is what + * goes out the wire. + */ +ATF_TC_WITHOUT_HEAD(local_broadcast); +ATF_TC_BODY(local_broadcast, tc) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + }; + socklen_t slen = sizeof(sin); + int s, l; + + s = bcastsock(); + l = bcastsock(); + ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0); + firstbcast(&sin.sin_addr); + + bcastecho(s, &sin, l); + + close(s); + close(l); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, INADDR_BROADCAST); + ATF_TP_ADD_TC(tp, IP_ONESBCAST); + ATF_TP_ADD_TC(tp, local_broadcast); + + return (atf_no_error()); +} diff --git a/tests/sys/netinet/carp.py b/tests/sys/netinet/carp.py index ac2c5547fe97..e35c9470d035 100644 --- a/tests/sys/netinet/carp.py +++ b/tests/sys/netinet/carp.py @@ -39,6 +39,7 @@ class TestCarp(VnetTestTemplate): if p.src != "00:00:5e:00:01:01": raise + @pytest.mark.require_progs(["scapy"]) def test_source_mac(self): "Test carp packets source address" @@ -51,3 +52,18 @@ class TestCarp(VnetTestTemplate): carp_pkts = sc.sniff(iface=if1.name, stop_filter=filter_f, timeout=5) self.check_carp_src_mac(carp_pkts) + + @pytest.mark.require_progs(["scapy"]) + def test_source_mac_vrrp(self): + "Test VRRP packets source address" + + if1 = self.vnet.iface_alias_map["if1"] + + ToolsHelper.print_output( + "ifconfig {} add vhid 1 carpver 3 192.0.2.203/24".format(if1.name) + ) + + carp_pkts = sc.sniff(iface=if1.name, stop_filter=filter_f, timeout=5) + + self.check_carp_src_mac(carp_pkts) + diff --git a/tests/sys/netinet/carp.sh b/tests/sys/netinet/carp.sh index d08940d433d7..2aae2854826e 100755 --- a/tests/sys/netinet/carp.sh +++ b/tests/sys/netinet/carp.sh @@ -31,7 +31,7 @@ is_master() jail=$1 itf=$2 - jexec ${jail} ifconfig ${itf} | grep carp | grep MASTER + jexec ${jail} ifconfig ${itf} | grep -E '(carp|vrrp)' | grep MASTER } wait_for_carp() @@ -71,6 +71,7 @@ basic_v4_head() basic_v4_body() { carp_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair_one=$(vnet_mkepair) @@ -105,6 +106,52 @@ basic_v4_cleanup() vnet_cleanup } +atf_test_case "vrrp_v4" "cleanup" +vrrp_v4_head() +{ + atf_set descr 'Basic VRRP test (IPv4)' + atf_set require.user root +} + +vrrp_v4_body() +{ + carp_init + vnet_init_bridge + + j=vrrp_basic_v4 + + bridge=$(vnet_mkbridge) + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + + vnet_mkjail ${j}_one ${bridge} ${epair_one}a ${epair_two}a + vnet_mkjail ${j}_two ${epair_one}b + vnet_mkjail ${j}_three ${epair_two}b + + jexec ${j}_one ifconfig ${bridge} 192.0.2.4/29 up + jexec ${j}_one ifconfig ${bridge} addm ${epair_one}a \ + addm ${epair_two}a + jexec ${j}_one ifconfig ${epair_one}a up + jexec ${j}_one ifconfig ${epair_two}a up + + jexec ${j}_two ifconfig ${epair_one}b 192.0.2.202/29 up + jexec ${j}_two ifconfig ${epair_one}b add vhid 1 carpver 3 192.0.2.1/29 + + jexec ${j}_three ifconfig ${epair_two}b 192.0.2.203/29 up + jexec ${j}_three ifconfig ${epair_two}b add vhid 1 carpver 3 \ + 192.0.2.1/29 + + wait_for_carp ${j}_two ${epair_one}b \ + ${j}_three ${epair_two}b + + atf_check -s exit:0 -o ignore jexec ${j}_one \ + ping -c 3 192.0.2.1 +} + +vrrp_v4_cleanup() +{ + vnet_cleanup +} atf_test_case "unicast_v4" "cleanup" unicast_v4_head() @@ -117,45 +164,53 @@ unicast_v4_body() { carp_init - bridge=$(vnet_mkbridge) epair_one=$(vnet_mkepair) epair_two=$(vnet_mkepair) - vnet_mkjail carp_uni_v4_one ${bridge} ${epair_one}a ${epair_two}a + vnet_mkjail carp_uni_v4_one ${epair_one}a ${epair_two}a vnet_mkjail carp_uni_v4_two ${epair_one}b vnet_mkjail carp_uni_v4_three ${epair_two}b - jexec carp_uni_v4_one ifconfig ${bridge} 192.0.2.4/29 up jexec carp_uni_v4_one sysctl net.inet.ip.forwarding=1 - jexec carp_uni_v4_one ifconfig ${bridge} addm ${epair_one}a \ - addm ${epair_two}a - jexec carp_uni_v4_one ifconfig ${epair_one}a up - jexec carp_uni_v4_one ifconfig ${epair_two}a up - jexec carp_uni_v4_one ifconfig ${bridge} inet alias 198.51.100.1/25 - jexec carp_uni_v4_one ifconfig ${bridge} inet alias 198.51.100.129/25 + jexec carp_uni_v4_one ifconfig ${epair_one}a inet 198.51.100.1/25 + jexec carp_uni_v4_one ifconfig ${epair_two}a inet 198.51.100.129/25 + jexec carp_uni_v4_two sysctl net.inet.ip.forwarding=1 jexec carp_uni_v4_two ifconfig ${epair_one}b 198.51.100.2/25 up - jexec carp_uni_v4_two route add default 198.51.100.1 + jexec carp_uni_v4_two route add 198.51.100.224 198.51.100.1 + # A peer address x.x.x.224 to catch PR 284872 jexec carp_uni_v4_two ifconfig ${epair_one}b add vhid 1 \ - peer 198.51.100.130 192.0.2.1/29 + peer 198.51.100.224 192.0.2.1/32 - jexec carp_uni_v4_three ifconfig ${epair_two}b 198.51.100.130/25 up - jexec carp_uni_v4_three route add default 198.51.100.129 + jexec carp_uni_v4_three sysctl net.inet.ip.forwarding=1 + jexec carp_uni_v4_three ifconfig ${epair_two}b 198.51.100.224/25 up + jexec carp_uni_v4_three route add 198.51.100.2 198.51.100.129 jexec carp_uni_v4_three ifconfig ${epair_two}b add vhid 1 \ - peer 198.51.100.2 192.0.2.1/29 + peer 198.51.100.2 192.0.2.1/32 # Sanity check atf_check -s exit:0 -o ignore jexec carp_uni_v4_two \ - ping -c 1 198.51.100.130 + ping -c 1 198.51.100.224 wait_for_carp carp_uni_v4_two ${epair_one}b \ carp_uni_v4_three ${epair_two}b + # Setup RIPv2 route daemon + jexec carp_uni_v4_two routed -s -Pripv2 + jexec carp_uni_v4_three routed -s -Pripv2 + jexec carp_uni_v4_one routed -Pripv2 + + # XXX Wait for route propagation + sleep 3 + atf_check -s exit:0 -o ignore jexec carp_uni_v4_one \ ping -c 3 192.0.2.1 - jexec carp_uni_v4_two ifconfig - jexec carp_uni_v4_three ifconfig + # Check that we remain in unicast when tweaking settings + atf_check -s exit:0 -o ignore \ + jexec carp_uni_v4_two ifconfig ${epair_one}b vhid 1 advskew 2 + atf_check -s exit:0 -o match:"peer 198.51.100.224" \ + jexec carp_uni_v4_two ifconfig ${epair_one}b } unicast_v4_cleanup() @@ -173,6 +228,7 @@ basic_v6_head() basic_v6_body() { carp_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair_one=$(vnet_mkepair) @@ -210,6 +266,56 @@ basic_v6_cleanup() vnet_cleanup } +atf_test_case "vrrp_v6" "cleanup" +vrrp_v6_head() +{ + atf_set descr 'Basic VRRP test (IPv6)' + atf_set require.user root +} + +vrrp_v6_body() +{ + carp_init + vnet_init_bridge + + j=carp_basic_v6 + + bridge=$(vnet_mkbridge) + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + + vnet_mkjail ${j}_one ${bridge} ${epair_one}a ${epair_two}a + vnet_mkjail ${j}_two ${epair_one}b + vnet_mkjail ${j}_three ${epair_two}b + + jexec ${j}_one ifconfig ${bridge} inet6 2001:db8::0:4/64 up \ + no_dad + jexec ${j}_one ifconfig ${bridge} addm ${epair_one}a \ + addm ${epair_two}a + jexec ${j}_one ifconfig ${epair_one}a up + jexec ${j}_one ifconfig ${epair_two}a up + + jexec ${j}_two ifconfig ${epair_one}b inet6 \ + 2001:db8::1:2/64 up no_dad + jexec ${j}_two ifconfig ${epair_one}b inet6 add vhid 1 carpver 3 \ + 2001:db8::0:1/64 + + jexec ${j}_three ifconfig ${epair_two}b inet6 2001:db8::1:3/64 up no_dad + jexec ${j}_three ifconfig ${epair_two}b inet6 add vhid 1 carpver 3 \ + 2001:db8::0:1/64 + + wait_for_carp ${j}_two ${epair_one}b \ + ${j}_three ${epair_two}b + + atf_check -s exit:0 -o ignore jexec ${j}_one \ + ping -6 -c 3 2001:db8::0:1 +} + +vrrp_v6_cleanup() +{ + vnet_cleanup +} + atf_test_case "unicast_v6" "cleanup" unicast_v6_head() { @@ -220,6 +326,7 @@ unicast_v6_head() unicast_v6_body() { carp_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair_one=$(vnet_mkepair) @@ -281,6 +388,7 @@ unicast_ll_v6_head() unicast_ll_v6_body() { carp_init + vnet_init_bridge j=carp_uni_ll_v6 @@ -391,6 +499,7 @@ nd6_ns_source_mac_head() nd6_ns_source_mac_body() { carp_init + vnet_init_bridge bridge=$(vnet_mkbridge) epair_one=$(vnet_mkepair) @@ -469,8 +578,10 @@ switch_cleanup() atf_init_test_cases() { atf_add_test_case "basic_v4" + atf_add_test_case "vrrp_v4" atf_add_test_case "unicast_v4" atf_add_test_case "basic_v6" + atf_add_test_case "vrrp_v6" atf_add_test_case "unicast_v6" atf_add_test_case "unicast_ll_v6" atf_add_test_case "negative_demotion" diff --git a/tests/sys/netinet/divert.sh b/tests/sys/netinet/divert.sh index c3b1896b995e..d50620d94a09 100755 --- a/tests/sys/netinet/divert.sh +++ b/tests/sys/netinet/divert.sh @@ -41,7 +41,7 @@ ipdivert_ip_output_remote_success_head() { atf_set descr 'Test diverting IPv4 packet to remote destination' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } ipdivert_ip_output_remote_success_body() { @@ -96,7 +96,7 @@ ipdivert_ip_input_local_success_head() { atf_set descr 'Test diverting IPv4 packet to remote destination' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } ipdivert_ip_input_local_success_body() { diff --git a/tests/sys/netinet/fibs_multibind_test.c b/tests/sys/netinet/fibs_multibind_test.c new file mode 100644 index 000000000000..61ebf83c56ef --- /dev/null +++ b/tests/sys/netinet/fibs_multibind_test.c @@ -0,0 +1,755 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024-2025 Stormshield + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/sysctl.h> + +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/ip_icmp.h> +#include <netinet/icmp6.h> + +#include <errno.h> +#include <fcntl.h> +#include <pwd.h> +#include <stdio.h> +#include <string.h> + +#include <atf-c.h> + +#define MAKETEST_TCP(name) \ +ATF_TC_WITHOUT_HEAD(name ## _tcp); \ +ATF_TC_BODY(name ## _tcp, tc) \ +{ \ + name(PF_INET, SOCK_STREAM, tc); \ +} \ +ATF_TC_WITHOUT_HEAD(name ## _tcp6); \ +ATF_TC_BODY(name ## _tcp6, tc) \ +{ \ + name(PF_INET6, SOCK_STREAM, tc); \ +} +#define MAKETEST_UDP(name) \ +ATF_TC_WITHOUT_HEAD(name ## _udp); \ +ATF_TC_BODY(name ## _udp, tc) \ +{ \ + name(PF_INET, SOCK_DGRAM, tc); \ +} \ +ATF_TC_WITHOUT_HEAD(name ## _udp6); \ +ATF_TC_BODY(name ## _udp6, tc) \ +{ \ + name(PF_INET6, SOCK_DGRAM, tc); \ +} +#define MAKETEST_RAW(name) \ +ATF_TC(name ## _raw); \ +ATF_TC_HEAD(name ## _raw, tc) \ +{ \ + atf_tc_set_md_var(tc, "require.user", \ + "root"); \ +} \ +ATF_TC_BODY(name ## _raw, tc) \ +{ \ + name(PF_INET, SOCK_RAW, tc); \ +} \ +ATF_TC(name ## _raw6); \ +ATF_TC_HEAD(name ## _raw6, tc) \ +{ \ + atf_tc_set_md_var(tc, "require.user", \ + "root"); \ +} \ +ATF_TC_BODY(name ## _raw6, tc) \ +{ \ + name(PF_INET6, SOCK_RAW, tc); \ +} + +#define MAKETEST(name) \ + MAKETEST_TCP(name) \ + MAKETEST_UDP(name) + +#define LISTTEST_TCP(name) \ + ATF_TP_ADD_TC(tp, name ## _tcp); \ + ATF_TP_ADD_TC(tp, name ## _tcp6); +#define LISTTEST_UDP(name) \ + ATF_TP_ADD_TC(tp, name ## _udp); \ + ATF_TP_ADD_TC(tp, name ## _udp6); +#define LISTTEST_RAW(name) \ + ATF_TP_ADD_TC(tp, name ## _raw); \ + ATF_TP_ADD_TC(tp, name ## _raw6); +#define LISTTEST(name) \ + LISTTEST_TCP(name) \ + LISTTEST_UDP(name) + +static void +checked_close(int s) +{ + int error; + + error = close(s); + ATF_REQUIRE_MSG(error == 0, "close failed: %s", strerror(errno)); +} + +static int +mksockp(int domain, int type, int fib, int proto) +{ + int error, s; + + s = socket(domain, type, proto); + ATF_REQUIRE(s != -1); + error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &fib, sizeof(fib)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + return (s); +} + +static int +mksock(int domain, int type, int fib) +{ + return (mksockp(domain, type, fib, 0)); +} + +static void +require_fibs_multibind(int socktype, int minfibs) +{ + const char *sysctl; + size_t sz; + int error, fibs, multibind; + + fibs = 0; + sz = sizeof(fibs); + error = sysctlbyname("net.fibs", &fibs, &sz, NULL, 0); + ATF_REQUIRE_MSG(error == 0, "sysctlbyname failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(fibs >= 1, "strange FIB count %d", fibs); + if (fibs == 1) + atf_tc_skip("multiple FIBs not enabled"); + if (fibs < minfibs) + atf_tc_skip("not enough FIBs, need %d", minfibs); + + switch (socktype) { + case SOCK_STREAM: + sysctl = "net.inet.tcp.bind_all_fibs"; + break; + case SOCK_DGRAM: + sysctl = "net.inet.udp.bind_all_fibs"; + break; + case SOCK_RAW: + sysctl = "net.inet.raw.bind_all_fibs"; + break; + default: + atf_tc_fail("unknown socket type %d", socktype); + break; + } + + multibind = -1; + sz = sizeof(multibind); + error = sysctlbyname(sysctl, &multibind, &sz, NULL, 0); + ATF_REQUIRE_MSG(error == 0, "sysctlbyname failed: %s", strerror(errno)); + if (multibind != 0) + atf_tc_skip("FIB multibind not configured (%s)", sysctl); +} + +/* + * Make sure that different users can't bind to the same port from different + * FIBs. + */ +static void +multibind_different_user(int domain, int type, const atf_tc_t *tc) +{ + struct sockaddr_storage ss; + struct passwd *passwd; + const char *user; + socklen_t sslen; + int error, s[2]; + + if (geteuid() != 0) + atf_tc_skip("need root privileges"); + if (!atf_tc_has_config_var(tc, "unprivileged_user")) + atf_tc_skip("unprivileged_user not set"); + + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + + require_fibs_multibind(type, 2); + + s[0] = mksock(domain, type, 0); + + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(s[0], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + error = getsockname(s[0], (struct sockaddr *)&ss, &sslen); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + /* + * Create a second socket in a different FIB, and bind it to the same + * address/port tuple. This should succeed if done as the same user as + * the first socket, and should fail otherwise. + */ + s[1] = mksock(domain, type, 1); + error = bind(s[1], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(close(s[1]) == 0, "close failed: %s", strerror(errno)); + + user = atf_tc_get_config_var(tc, "unprivileged_user"); + passwd = getpwnam(user); + ATF_REQUIRE(passwd != NULL); + error = seteuid(passwd->pw_uid); + ATF_REQUIRE_MSG(error == 0, "seteuid failed: %s", strerror(errno)); + + /* Repeat the bind as a different user. */ + s[1] = mksock(domain, type, 1); + error = bind(s[1], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_ERRNO(EADDRINUSE, error == -1); + ATF_REQUIRE_MSG(close(s[1]) == 0, "close failed: %s", strerror(errno)); +} +MAKETEST(multibind_different_user); + +/* + * Verify that a listening socket only accepts connections originating from the + * same FIB. + */ +static void +per_fib_listening_socket(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_storage ss; + socklen_t sslen; + int cs1, cs2, error, fib1, fib2, ls1, ls2, ns; + + ATF_REQUIRE(type == SOCK_STREAM); + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + fib1 = 0; + fib2 = 1; + + ls1 = mksock(domain, type, fib1); + ls2 = mksock(domain, type, fib2); + + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(ls1, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + error = getsockname(ls1, (struct sockaddr *)&ss, &sslen); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + error = listen(ls1, 5); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + cs1 = mksock(domain, type, fib1); + cs2 = mksock(domain, type, fib2); + + /* + * Make sure we can connect from the same FIB. + */ + error = connect(cs1, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno)); + ns = accept(ls1, NULL, NULL); + ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno)); + checked_close(ns); + checked_close(cs1); + cs1 = mksock(domain, type, fib1); + + /* + * ... but not from a different FIB. + */ + error = connect(cs2, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == -1, "connect succeeded unexpectedly"); + ATF_REQUIRE_MSG(errno == ECONNREFUSED, "unexpected error %d", errno); + checked_close(cs2); + cs2 = mksock(domain, type, fib2); + + /* + * ... but if there are multiple listening sockets, we always connect to + * the same FIB. + */ + error = bind(ls2, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(ls2, 5); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + for (int i = 0; i < 10; i++) { + error = connect(cs1, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", + strerror(errno)); + ns = accept(ls1, NULL, NULL); + ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno)); + + checked_close(ns); + checked_close(cs1); + cs1 = mksock(domain, type, fib1); + } + for (int i = 0; i < 10; i++) { + error = connect(cs2, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", + strerror(errno)); + ns = accept(ls2, NULL, NULL); + ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno)); + + checked_close(ns); + checked_close(cs2); + cs2 = mksock(domain, type, fib2); + } + + /* + * ... and if we close one of the listening sockets, we're back to only + * being able to connect from the same FIB. + */ + checked_close(ls1); + error = connect(cs1, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == -1, "connect succeeded unexpectedly"); + ATF_REQUIRE_MSG(errno == ECONNREFUSED, "unexpected error %d", errno); + checked_close(cs1); + + error = connect(cs2, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno)); + ns = accept(ls2, NULL, NULL); + ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno)); + checked_close(ns); + checked_close(cs2); + checked_close(ls2); +} +MAKETEST_TCP(per_fib_listening_socket); + +/* + * Verify that a bound datagram socket only accepts data from the same FIB. + */ +static void +per_fib_dgram_socket(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_storage ss; + struct sockaddr_in6 *sin6p; + socklen_t sslen; + ssize_t n; + int error, cs1, cs2, fib1, fib2, ss1, ss2; + char b; + + ATF_REQUIRE(type == SOCK_DGRAM); + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + fib1 = 0; + fib2 = 1; + + cs1 = mksock(domain, type, fib1); + cs2 = mksock(domain, type, fib2); + + ss1 = mksock(domain, type, fib1); + ss2 = mksock(domain, type, fib2); + + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(ss1, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + error = getsockname(ss1, (struct sockaddr *)&ss, &sslen); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + if (domain == PF_INET6) { + sin6p = (struct sockaddr_in6 *)&ss; + sin6p->sin6_addr = in6addr_loopback; + } + + /* If we send a byte from cs1, it should be recieved by ss1. */ + b = 42; + n = sendto(cs1, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + n = recv(ss1, &b, sizeof(b), 0); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 42); + + /* If we send a byte from cs2, it should not be received by ss1. */ + b = 42; + n = sendto(cs2, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + usleep(10000); + n = recv(ss1, &b, sizeof(b), MSG_DONTWAIT); + ATF_REQUIRE_ERRNO(EWOULDBLOCK, n == -1); + + error = bind(ss2, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + /* Repeat now that ss2 is bound. */ + b = 42; + n = sendto(cs1, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + n = recv(ss1, &b, sizeof(b), 0); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 42); + + b = 42; + n = sendto(cs2, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + n = recv(ss2, &b, sizeof(b), 0); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 42); + + checked_close(ss1); + checked_close(ss2); + checked_close(cs1); + checked_close(cs2); +} +MAKETEST_UDP(per_fib_dgram_socket); + +static size_t +ping(int s, const struct sockaddr *sa, socklen_t salen) +{ + struct { + struct icmphdr icmp; + char data[64]; + } icmp; + ssize_t n; + + memset(&icmp, 0, sizeof(icmp)); + icmp.icmp.icmp_type = ICMP_ECHO; + icmp.icmp.icmp_code = 0; + icmp.icmp.icmp_cksum = htons((unsigned short)~(ICMP_ECHO << 8)); + n = sendto(s, &icmp, sizeof(icmp), 0, sa, salen); + ATF_REQUIRE_MSG(n == (ssize_t)sizeof(icmp), "sendto failed: %s", + strerror(errno)); + + return (sizeof(icmp) + sizeof(struct ip)); +} + +static size_t +ping6(int s, const struct sockaddr *sa, socklen_t salen) +{ + struct { + struct icmp6_hdr icmp6; + char data[64]; + } icmp6; + ssize_t n; + + memset(&icmp6, 0, sizeof(icmp6)); + icmp6.icmp6.icmp6_type = ICMP6_ECHO_REQUEST; + icmp6.icmp6.icmp6_code = 0; + icmp6.icmp6.icmp6_cksum = + htons((unsigned short)~(ICMP6_ECHO_REQUEST << 8)); + n = sendto(s, &icmp6, sizeof(icmp6), 0, sa, salen); + ATF_REQUIRE_MSG(n == (ssize_t)sizeof(icmp6), "sendto failed: %s", + strerror(errno)); + + return (sizeof(icmp6)); +} + +static void +per_fib_raw_socket(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + ssize_t n; + size_t sz; + int error, cs, s[2], proto; + uint8_t b[256]; + + ATF_REQUIRE(type == SOCK_RAW); + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + proto = domain == PF_INET ? IPPROTO_ICMP : IPPROTO_ICMPV6; + s[0] = mksockp(domain, type, 0, proto); + s[1] = mksockp(domain, type, 1, proto); + + if (domain == PF_INET) { + memset(&sin, 0, sizeof(sin)); + sin.sin_family = domain; + sin.sin_len = sizeof(sin); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + error = bind(s[0], (struct sockaddr *)&sin, sizeof(sin)); + } else /* if (domain == PF_INET6) */ { + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = domain; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_addr = in6addr_loopback; + error = bind(s[0], (struct sockaddr *)&sin6, sizeof(sin6)); + } + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + for (int i = 0; i < 2; i++) { + cs = mksockp(domain, type, i, proto); + if (domain == PF_INET) { + sz = ping(cs, (struct sockaddr *)&sin, sizeof(sin)); + } else /* if (domain == PF_INET6) */ { + sz = ping6(cs, (struct sockaddr *)&sin6, sizeof(sin6)); + } + n = recv(s[i], b, sizeof(b), 0); + ATF_REQUIRE_MSG(n > 0, "recv failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(n == (ssize_t)sz, + "short packet received: %zd", n); + + if (domain == PF_INET6) { + /* Get the echo reply as well. */ + n = recv(s[i], b, sizeof(b), 0); + ATF_REQUIRE_MSG(n > 0, + "recv failed: %s", strerror(errno)); + ATF_REQUIRE_MSG(n == (ssize_t)sz, + "short packet received: %zd", n); + } + + /* Make sure that the other socket didn't receive anything. */ + n = recv(s[1 - i], b, sizeof(b), MSG_DONTWAIT); + printf("n = %zd i = %d\n", n, i); + ATF_REQUIRE_ERRNO(EWOULDBLOCK, n == -1); + + checked_close(cs); + } + + checked_close(s[0]); + checked_close(s[1]); +} +MAKETEST_RAW(per_fib_raw_socket); + +/* + * Create a pair of load-balancing listening socket groups, one in each FIB, and + * make sure that connections to the group are only load-balanced within the + * same FIB. + */ +static void +multibind_lbgroup_stream(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_storage ss; + socklen_t sslen; + int error, as, cs, s[3]; + + ATF_REQUIRE(type == SOCK_STREAM); + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + s[0] = mksock(domain, type, 0); + ATF_REQUIRE(setsockopt(s[0], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + ATF_REQUIRE(fcntl(s[0], F_SETFL, O_NONBLOCK) == 0); + s[1] = mksock(domain, type, 0); + ATF_REQUIRE(setsockopt(s[1], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + ATF_REQUIRE(fcntl(s[1], F_SETFL, O_NONBLOCK) == 0); + s[2] = mksock(domain, type, 1); + ATF_REQUIRE(setsockopt(s[2], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(s[0], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(s[0], 5); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + error = getsockname(s[0], (struct sockaddr *)&ss, &sslen); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + error = bind(s[1], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(s[1], 5); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + error = bind(s[2], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(s[2], 5); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + /* + * Initiate connections from FIB 0, make sure they go to s[0] or s[1]. + */ + for (int count = 0; count < 100; count++) { + cs = mksock(domain, type, 0); + error = connect(cs, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", + strerror(errno)); + + do { + as = accept(s[0], NULL, NULL); + if (as == -1) { + ATF_REQUIRE_MSG(errno == EWOULDBLOCK, + "accept failed: %s", strerror(errno)); + as = accept(s[1], NULL, NULL); + if (as == -1) { + ATF_REQUIRE_MSG(errno == EWOULDBLOCK, + "accept failed: %s", + strerror(errno)); + } + } + } while (as == -1); + checked_close(as); + checked_close(cs); + } + + /* + * Initiate connections from FIB 1, make sure they go to s[2]. + */ + for (int count = 0; count < 100; count++) { + cs = mksock(domain, type, 1); + error = connect(cs, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", + strerror(errno)); + + as = accept(s[2], NULL, NULL); + ATF_REQUIRE_MSG(as != -1, "accept failed: %s", strerror(errno)); + checked_close(as); + checked_close(cs); + } + + checked_close(s[0]); + checked_close(s[1]); + checked_close(s[2]); +} +MAKETEST_TCP(multibind_lbgroup_stream); + +static void +multibind_lbgroup_dgram(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_storage ss; + struct sockaddr_in6 *sin6p; + socklen_t sslen; + ssize_t n; + int error, cs, s[3]; + char b; + + ATF_REQUIRE(type == SOCK_DGRAM); + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + s[0] = mksock(domain, type, 0); + ATF_REQUIRE(setsockopt(s[0], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + s[1] = mksock(domain, type, 0); + ATF_REQUIRE(setsockopt(s[1], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + s[2] = mksock(domain, type, 1); + ATF_REQUIRE(setsockopt(s[2], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1}, + sizeof(int)) == 0); + + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(s[0], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = getsockname(s[0], (struct sockaddr *)&ss, &sslen); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + + error = bind(s[1], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = bind(s[2], (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + if (domain == PF_INET6) { + sin6p = (struct sockaddr_in6 *)&ss; + sin6p->sin6_addr = in6addr_loopback; + } + + /* + * Send a packet from FIB 0, make sure it goes to s[0] or s[1]. + */ + cs = mksock(domain, type, 0); + for (int count = 0; count < 100; count++) { + int bytes, rs; + + b = 42; + n = sendto(cs, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + usleep(1000); + + error = ioctl(s[0], FIONREAD, &bytes); + ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s", + strerror(errno)); + if (bytes == 0) { + error = ioctl(s[1], FIONREAD, &bytes); + ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s", + strerror(errno)); + rs = s[1]; + } else { + rs = s[0]; + } + n = recv(rs, &b, sizeof(b), 0); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 42); + ATF_REQUIRE(bytes == 1); + } + checked_close(cs); + + /* + * Send a packet from FIB 1, make sure it goes to s[2]. + */ + cs = mksock(domain, type, 1); + for (int count = 0; count < 100; count++) { + b = 42; + n = sendto(cs, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno)); + usleep(1000); + + n = recv(s[2], &b, sizeof(b), 0); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 42); + } + checked_close(cs); + + checked_close(s[0]); + checked_close(s[1]); + checked_close(s[2]); +} +MAKETEST_UDP(multibind_lbgroup_dgram); + +/* + * Make sure that we can't change the FIB of a bound socket. + */ +static void +no_setfib_after_bind(int domain, int type, const atf_tc_t *tc __unused) +{ + struct sockaddr_storage ss; + socklen_t sslen; + int error, s; + + ATF_REQUIRE(domain == PF_INET || domain == PF_INET6); + require_fibs_multibind(type, 2); + + s = mksock(domain, type, 0); + + sslen = domain == PF_INET ? sizeof(struct sockaddr_in) : + sizeof(struct sockaddr_in6); + memset(&ss, 0, sizeof(ss)); + ss.ss_family = domain; + ss.ss_len = sslen; + error = bind(s, (struct sockaddr *)&ss, sslen); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + + error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &(int){1}, sizeof(int)); + ATF_REQUIRE_ERRNO(EISCONN, error == -1); + + /* It's ok to set the FIB number to its current value. */ + error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &(int){0}, sizeof(int)); + ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno)); + + checked_close(s); +} +MAKETEST(no_setfib_after_bind); + +ATF_TP_ADD_TCS(tp) +{ + LISTTEST(multibind_different_user); + LISTTEST_TCP(per_fib_listening_socket); + LISTTEST_UDP(per_fib_dgram_socket); + LISTTEST_RAW(per_fib_raw_socket); + LISTTEST_TCP(multibind_lbgroup_stream); + LISTTEST_UDP(multibind_lbgroup_dgram); + LISTTEST(no_setfib_after_bind); + + return (atf_no_error()); +} diff --git a/tests/sys/netinet/fibs_test.sh b/tests/sys/netinet/fibs_test.sh index 5c1a918abb2c..2d0b63f8e30a 100644 --- a/tests/sys/netinet/fibs_test.sh +++ b/tests/sys/netinet/fibs_test.sh @@ -30,9 +30,6 @@ # Authors: Alan Somers (Spectra Logic Corporation) # -# All of the tests in this file requires the test-suite config variable "fibs" -# to be defined to a space-delimited list of FIBs that may be used for testing. - # arpresolve should check the interface fib for routes to a target when # creating an ARP table entry. This is a regression for kern/167947, where # arpresolve only checked the default route. @@ -48,7 +45,6 @@ arpresolve_checks_interface_fib_head() { atf_set "descr" "arpresolve should check the interface fib, not the default fib, for routes" atf_set "require.user" "root" - atf_set "require.config" "fibs" atf_set "require.progs" "nping" } arpresolve_checks_interface_fib_body() @@ -100,7 +96,6 @@ loopback_and_network_routes_on_nondefault_fib_head() { atf_set "descr" "When creating and deleting loopback IPv4 routes, use the interface's fib" atf_set "require.user" "root" - atf_set "require.config" "fibs" } loopback_and_network_routes_on_nondefault_fib_body() @@ -157,7 +152,6 @@ loopback_and_network_routes_on_nondefault_fib_inet6_head() { atf_set "descr" "When creating and deleting loopback IPv6 routes, use the interface's fib" atf_set "require.user" "root" - atf_set "require.config" "fibs" } loopback_and_network_routes_on_nondefault_fib_inet6_body() @@ -216,7 +210,6 @@ default_route_with_multiple_fibs_on_same_subnet_head() { atf_set "descr" "Multiple interfaces on the same subnet but with different fibs can both have default IPv4 routes" atf_set "require.user" "root" - atf_set "require.config" "fibs" } default_route_with_multiple_fibs_on_same_subnet_body() @@ -263,7 +256,6 @@ default_route_with_multiple_fibs_on_same_subnet_inet6_head() { atf_set "descr" "Multiple interfaces on the same subnet but with different fibs can both have default IPv6 routes" atf_set "require.user" "root" - atf_set "require.config" "fibs" } default_route_with_multiple_fibs_on_same_subnet_inet6_body() @@ -315,7 +307,6 @@ same_ip_multiple_ifaces_fib0_head() { atf_set "descr" "Can remove an IPv4 alias from an interface when the same IPv4 is also assigned to another interface." atf_set "require.user" "root" - atf_set "require.config" "fibs" } same_ip_multiple_ifaces_fib0_body() { @@ -329,6 +320,9 @@ same_ip_multiple_ifaces_fib0_body() # Setup the interfaces, then remove one alias. It should not panic. setup_tap 0 inet ${ADDR} ${MASK0} TAP0=${TAP} + # After commit 361a8395f0b0e6f254fd138798232529679d99f6 it became + # an error to assign the same interface address twice. + atf_expect_fail "The test results in an ifconfig error and thus spuriously fails" setup_tap 0 inet ${ADDR} ${MASK1} TAP1=${TAP} ifconfig ${TAP1} -alias ${ADDR} @@ -358,18 +352,16 @@ same_ip_multiple_ifaces_head() { atf_set "descr" "Can remove an IPv4 alias from an interface when the same address is also assigned to another interface, on non-default FIBs." atf_set "require.user" "root" - atf_set "require.config" "fibs" } same_ip_multiple_ifaces_body() { - atf_expect_fail "kern/189088 Assigning the same IP to multiple interfaces in different FIBs creates a host route for only one" ADDR="192.0.2.2" MASK0="24" MASK1="32" # Unlike most of the tests in this file, this is applicable regardless # of net.add_addr_allfibs - get_fibs 2 + get_fibs 4 # Setup the interfaces, then remove one alias. It should not panic. setup_tap ${FIB0} inet ${ADDR} ${MASK0} @@ -381,13 +373,13 @@ same_ip_multiple_ifaces_body() setfib ${FIB1} netstat -rn -f inet # Do it again, in the opposite order. It should not panic. - setup_tap ${FIB0} inet ${ADDR} ${MASK0} + setup_tap ${FIB2} inet ${ADDR} ${MASK0} TAP0=${TAP} - setup_tap ${FIB1} inet ${ADDR} ${MASK1} + setup_tap ${FIB3} inet ${ADDR} ${MASK1} TAP1=${TAP} ifconfig ${TAP0} -alias ${ADDR} atf_check -o not-match:"^${ADDR}[[:space:]]" \ - setfib ${FIB0} netstat -rn -f inet + setfib ${FIB2} netstat -rn -f inet } same_ip_multiple_ifaces_cleanup() { @@ -404,7 +396,6 @@ same_ip_multiple_ifaces_inet6_head() { atf_set "descr" "Can remove an IPv6 alias from an interface when the same address is also assigned to another interface, on non-default FIBs." atf_set "require.user" "root" - atf_set "require.config" "fibs" } same_ip_multiple_ifaces_inet6_body() { @@ -446,7 +437,7 @@ slaac_on_nondefault_fib6_head() { atf_set "descr" "SLAAC correctly installs routes on non-default FIBs" atf_set "require.user" "root" - atf_set "require.config" "fibs" "allow_sysctl_side_effects" + atf_set "require.config" "allow_sysctl_side_effects" } slaac_on_nondefault_fib6_body() { @@ -533,7 +524,6 @@ subnet_route_with_multiple_fibs_on_same_subnet_head() { atf_set "descr" "Multiple FIBs can have IPv4 subnet routes for the same subnet" atf_set "require.user" "root" - atf_set "require.config" "fibs" } subnet_route_with_multiple_fibs_on_same_subnet_body() @@ -570,7 +560,6 @@ subnet_route_with_multiple_fibs_on_same_subnet_inet6_head() { atf_set "descr" "Multiple FIBs can have IPv6 subnet routes for the same subnet" atf_set "require.user" "root" - atf_set "require.config" "fibs" } subnet_route_with_multiple_fibs_on_same_subnet_inet6_body() @@ -620,7 +609,6 @@ udp_dontroute_head() { atf_set "descr" "Source address selection for UDP packets with SO_DONTROUTE on non-default FIBs works" atf_set "require.user" "root" - atf_set "require.config" "fibs" } udp_dontroute_body() @@ -671,7 +659,6 @@ udp_dontroute6_head() { atf_set "descr" "Source address selection for UDP IPv6 packets with SO_DONTROUTE on non-default FIBs works" atf_set "require.user" "root" - atf_set "require.config" "fibs" } udp_dontroute6_body() @@ -748,15 +735,13 @@ get_fibs() { NUMFIBS=$1 net_fibs=`sysctl -n net.fibs` + if [ $net_fibs -lt $(($NUMFIBS + 1)) ]; then + atf_check -o ignore sysctl net.fibs=$(($NUMFIBS + 1)) + net_fibs=`sysctl -n net.fibs` + fi i=0 while [ $i -lt "$NUMFIBS" ]; do - fib=`atf_config_get "fibs" | \ - awk -v i=$(( i + 1 )) '{print $i}'` - echo "fib is ${fib}" - eval FIB${i}=${fib} - if [ "$fib" -ge "$net_fibs" ]; then - atf_skip "The ${i}th configured fib is ${fib}, which is not less than net.fibs, which is ${net_fibs}" - fi + eval FIB${i}=$(($i + 1)) i=$(( $i + 1 )) done } @@ -816,9 +801,7 @@ setup_iface() local ADDR=$4 local MASK=$5 local FLAGS=$6 - echo setfib ${FIB} \ - ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS - setfib ${FIB} ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS + atf_check setfib ${FIB} ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS } # Create a tap(4) interface, configure it, and register it for cleanup. diff --git a/tests/sys/netinet/forward.sh b/tests/sys/netinet/forward.sh index e16927a27d07..be69e91b6137 100755 --- a/tests/sys/netinet/forward.sh +++ b/tests/sys/netinet/forward.sh @@ -34,7 +34,7 @@ fwd_ip_icmp_iface_fast_success_head() { atf_set descr 'Test valid IPv4 on-stick fastforwarding to iface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip_icmp_iface_fast_success_body() { @@ -90,7 +90,7 @@ fwd_ip_icmp_gw_fast_success_head() { atf_set descr 'Test valid IPv4 on-stick fastforwarding to gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip_icmp_gw_fast_success_body() { @@ -150,7 +150,7 @@ fwd_ip_icmp_iface_slow_success_head() { atf_set descr 'Test valid IPv4 on-stick "slow" forwarding to iface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip_icmp_iface_slow_success_body() { @@ -204,7 +204,7 @@ fwd_ip_icmp_gw_slow_success_head() { atf_set descr 'Test valid IPv4 on-stick "slow" forwarding to gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip_icmp_gw_slow_success_body() { @@ -259,6 +259,58 @@ fwd_ip_icmp_gw_slow_success_cleanup() { vnet_cleanup } +atf_test_case "fwd_ip_blackhole" "cleanup" +fwd_ip_blackhole_head() { + + atf_set descr 'Test blackhole routes' + atf_set require.user root +} + +fwd_ip_blackhole_body() { + jname="v4t-fwd_ip_blackhole" + + vnet_init + + epair=$(vnet_mkepair) + epair_out=$(vnet_mkepair) + + ifconfig ${epair}a 192.0.2.2/24 up + + vnet_mkjail ${jname} ${epair}b ${epair_out}b + jexec ${jname} ifconfig lo0 127.0.0.1/8 up + jexec ${jname} ifconfig ${epair}b 192.0.2.1/24 up + jexec ${jname} ifconfig ${epair_out}b 198.51.100.1/24 up + jexec ${jname} sysctl net.inet.ip.forwarding=1 + + route add default 192.0.2.1 + + atf_check -s exit:2 -o ignore \ + ping -c 1 -t 1 198.51.100.2 + atf_check -s exit:0 -o match:"0 packets not forwardable" \ + jexec ${jname} netstat -s -p ip + + # Create blackhole route + jexec ${jname} /sbin/route add 198.51.100.2 -blackhole -fib 0 + jexec ${jname} netstat -rn + + # Include an IP option to ensure slow path + atf_check -s exit:2 -o ignore \ + ping -c 1 -t 1 -R 198.51.100.2 + atf_check -s exit:0 -o match:"1 packet not forwardable" \ + jexec ${jname} netstat -s -p ip + + # Now try via the fast path + atf_check -s exit:2 -o ignore \ + ping -c 1 -t 1 198.51.100.2 + atf_check -s exit:0 -o match:"2 packets not forwardable" \ + jexec ${jname} netstat -s -p ip +} + +fwd_ip_blackhole_cleanup() { + + vnet_cleanup +} + atf_init_test_cases() { @@ -266,6 +318,7 @@ atf_init_test_cases() atf_add_test_case "fwd_ip_icmp_gw_fast_success" atf_add_test_case "fwd_ip_icmp_iface_slow_success" atf_add_test_case "fwd_ip_icmp_gw_slow_success" + atf_add_test_case "fwd_ip_blackhole" } # end diff --git a/tests/sys/netinet/igmp.py b/tests/sys/netinet/igmp.py index b079c5d18664..feb9b8b571d5 100644 --- a/tests/sys/netinet/igmp.py +++ b/tests/sys/netinet/igmp.py @@ -36,7 +36,6 @@ logging.getLogger("scapy").setLevel(logging.CRITICAL) curdir = os.path.dirname(os.path.realpath(__file__)) netpfil_common = curdir + "/../netpfil/common" sys.path.append(netpfil_common) -from sniffer import Sniffer sc = None sp = None @@ -63,6 +62,25 @@ def check_igmpv3(args, pkt): return True +def check_igmpv2(args, pkt): + pkt.show() + + igmp = pkt.getlayer(sc.igmp.IGMP) + if igmp is None: + return False + + if igmp.gaddr != args["group"]: + return False + + if args["type"] == "join": + if igmp.type != 0x16: + return False + if args["type"] == "leave": + if igmp.type != 0x17: + return False + + return True + class TestIGMP(VnetTestTemplate): REQUIRED_MODULES = [] TOPOLOGY = { @@ -81,12 +99,14 @@ class TestIGMP(VnetTestTemplate): sp = _sp super().setup_method(method) + @pytest.mark.require_progs(["scapy"]) def test_igmp3_join_leave(self): - "Test that we send the expected join/leave IGMPv2 messages" + "Test that we send the expected join/leave IGMPv3 messages" if1 = self.vnet.iface_alias_map["if1"] # Start a background sniff + from sniffer import Sniffer expected_pkt = { "type": "join", "group": "230.0.0.1" } sniffer = Sniffer(expected_pkt, check_igmpv3, if1.name, timeout=10) @@ -106,3 +126,32 @@ class TestIGMP(VnetTestTemplate): s.close() sniffer.join() assert(sniffer.correctPackets > 0) + + @pytest.mark.require_progs(["scapy"]) + def test_igmp2_join_leave(self): + "Test that we send the expected join/leave IGMPv2 messages" + ToolsHelper.print_output("/sbin/sysctl net.inet.igmp.default_version=2") + + if1 = self.vnet.iface_alias_map["if1"] + + # Start a background sniff + from sniffer import Sniffer + expected_pkt = { "type": "join", "group": "230.0.0.1" } + sniffer = Sniffer(expected_pkt, check_igmpv2, if1.name, timeout=10) + + # Now join a multicast group, and see if we're getting the igmp packet we expect + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + mreq = struct.pack("4sl", socket.inet_aton('230.0.0.1'), socket.INADDR_ANY) + s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + + # Wait for the sniffer to see the join packet + sniffer.join() + assert(sniffer.correctPackets > 0) + + # Now leave, check for the packet + expected_pkt = { "type": "leave", "group": "230.0.0.1" } + sniffer = Sniffer(expected_pkt, check_igmpv2, if1.name) + + s.close() + sniffer.join() + assert(sniffer.correctPackets > 0) diff --git a/tests/sys/netinet/ip_reass_test.c b/tests/sys/netinet/ip_reass_test.c index a65bfa34e1d4..538815bd7a2c 100644 --- a/tests/sys/netinet/ip_reass_test.c +++ b/tests/sys/netinet/ip_reass_test.c @@ -60,12 +60,16 @@ update_cksum(struct ip *ip) { size_t i; uint32_t cksum; - uint16_t *cksump; + uint8_t *cksump; + uint16_t tmp; ip->ip_sum = 0; - cksump = (uint16_t *)ip; - for (cksum = 0, i = 0; i < sizeof(*ip) / sizeof(*cksump); cksump++, i++) - cksum += ntohs(*cksump); + cksump = (char *)ip; + for (cksum = 0, i = 0; i < sizeof(*ip) / sizeof(uint16_t); i++) { + tmp = *cksump++; + tmp = tmp << 8 | *cksump++; + cksum += ntohs(tmp); + } cksum = (cksum >> 16) + (cksum & 0xffff); cksum = ~(cksum + (cksum >> 16)); ip->ip_sum = htons((uint16_t)cksum); diff --git a/tests/sys/netinet/libalias/2_natout.c b/tests/sys/netinet/libalias/2_natout.c index c6f5797b2db7..24ca06d11bf4 100644 --- a/tests/sys/netinet/libalias/2_natout.c +++ b/tests/sys/netinet/libalias/2_natout.c @@ -359,6 +359,172 @@ ATF_TC_BODY(8_portrange, dummy) LibAliasUninit(la); } +ATF_TC_WITHOUT_HEAD(9_udp_eim_mapping); +ATF_TC_BODY(9_udp_eim_mapping, dummy) +{ + struct libalias *la = LibAliasInit(NULL); + struct ip *po, *po2, *po3; + struct udphdr *uo, *uo2, *uo3; + uint16_t sport = 0x1234; + uint16_t dport = 0x5678; + uint16_t dport2 = 0x6789; + uint16_t aport, aport2, aport3; + + ATF_REQUIRE(la != NULL); + LibAliasSetAddress(la, masq); + LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0); + + po = ip_packet(0, 64); + UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq); + aport = ntohs(uo->uh_sport); + + /* Change of dst port shouldn't change alias port */ + po2 = ip_packet(0, 64); + UDP_NAT_CHECK(po2, uo2, prv1, sport, ext, dport2, masq); + aport2 = ntohs(uo2->uh_sport); + ATF_CHECK_EQ_MSG(aport, aport2, + "NAT uses address- and port-dependent mapping (%uh -> %uh)", + aport, aport2); + + /* Change of dst address shouldn't change alias port */ + po3 = ip_packet(0, 64); + UDP_NAT_CHECK(po3, uo3, prv1, sport, pub, dport, masq); + aport3 = ntohs(uo3->uh_sport); + ATF_CHECK_EQ_MSG(aport, aport3, "NAT uses address-dependent mapping"); + + free(po); + free(po2); + free(po3); + LibAliasUninit(la); +} + +ATF_TC_WITHOUT_HEAD(10_udp_eim_out_in); +ATF_TC_BODY(10_udp_eim_out_in, dummy) +{ + struct libalias *la = LibAliasInit(NULL); + struct ip *po, *po2, *po3; + struct udphdr *uo, *uo2, *uo3; + uint16_t sport = 0x1234; + uint16_t dport = 0x5678; + uint16_t dport2 = 0x6789; + uint16_t aport; + + ATF_REQUIRE(la != NULL); + LibAliasSetAddress(la, masq); + LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0); + + po = ip_packet(0, 64); + UDP_NAT_CHECK(po, uo, prv1, sport, pub, dport, masq); + aport = ntohs(uo->uh_sport); + + /* Accepts inbound packets from different port */ + po2 = ip_packet(0, 64); + UDP_UNNAT_CHECK(po2, uo2, pub, dport2, masq, aport, prv1, sport); + + /* Accepts inbound packets from differerent host and port */ + po3 = ip_packet(0, 64); + UDP_UNNAT_CHECK(po3, uo3, pub2, dport2, masq, aport, prv1, sport); + + free(po); + free(po2); + free(po3); + LibAliasUninit(la); +} + +ATF_TC_WITHOUT_HEAD(11_udp_eim_with_deny_incoming); +ATF_TC_BODY(11_udp_eim_with_deny_incoming, dummy) +{ + struct libalias *la = LibAliasInit(NULL); + struct ip *po, *po2, *po3, *po4; + struct udphdr *uo; + uint16_t sport = 0x1234; + uint16_t dport = 0x5678; + uint16_t dport2 = 0x6789; + uint16_t aport; + int ret; + + ATF_REQUIRE(la != NULL); + LibAliasSetAddress(la, masq); + LibAliasSetMode(la, + PKT_ALIAS_UDP_EIM | PKT_ALIAS_DENY_INCOMING, + ~0); + + po = ip_packet(0, 64); + UDP_NAT_CHECK(po, uo, prv1, sport, pub, dport, masq); + aport = ntohs(uo->uh_sport); + + po2 = ip_packet(0, 64); + po2->ip_src = pub; + po2->ip_dst = masq; + set_udp(po2, dport, aport); + ret = LibAliasIn(la, po2, 64); + ATF_CHECK_EQ_MSG(PKT_ALIAS_OK, ret, + "LibAliasIn failed with error %d\n", ret); + + po3 = ip_packet(0, 64); + po3->ip_src = pub; + po3->ip_dst = masq; + set_udp(po3, dport2, aport); + ret = LibAliasIn(la, po3, 64); + ATF_CHECK_EQ_MSG(PKT_ALIAS_IGNORED, ret, + "incoming packet from different port not ignored " + "with PKT_ALIAS_DENY_INCOMING"); + + po4 = ip_packet(0, 64); + po4->ip_src = pub2; + po4->ip_dst = masq; + set_udp(po4, dport2, aport); + ret = LibAliasIn(la, po4, 64); + ATF_CHECK_EQ_MSG(PKT_ALIAS_IGNORED, ret, + "incoming packet from different address and port not ignored " + "with PKT_ALIAS_DENY_INCOMING"); + + free(po); + free(po2); + free(po3); + free(po4); + LibAliasUninit(la); +} + +ATF_TC_WITHOUT_HEAD(12_udp_eim_hairpinning); +ATF_TC_BODY(12_udp_eim_hairpinning, dummy) +{ + struct libalias *la = LibAliasInit(NULL); + struct ip *po, *po2, *po3; + struct udphdr *uo, *uo2, *uo3; + uint16_t sport1 = 0x1234; + uint16_t sport2 = 0x2345; + uint16_t dport = 0x5678; + uint16_t extport1, extport2; + + ATF_REQUIRE(la != NULL); + LibAliasSetAddress(la, masq); + LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0); + + /* prv1 sends out somewhere (eg. a STUN server) */ + po = ip_packet(0, 64); + UDP_NAT_CHECK(po, uo, prv1, sport1, pub, dport, masq); + extport1 = ntohs(uo->uh_sport); + + /* prv2, behind the same NAT as prv1, also sends out somewhere */ + po2 = ip_packet(0, 64); + UDP_NAT_CHECK(po2, uo2, prv2, sport2, pub, dport, masq); + extport2 = ntohs(uo2->uh_sport); + + /* hairpin: prv1 sends to prv2's external NAT mapping + * (unaware it could address it internally instead). + */ + po3 = ip_packet(0, 64); + UDP_NAT_CHECK(po3, uo3, prv1, sport1, masq, extport2, masq); + UDP_UNNAT_CHECK(po3, uo3, masq, extport1, masq, extport2, + prv2, sport2); + + free(po); + free(po2); + free(po3); + LibAliasUninit(la); +} + ATF_TP_ADD_TCS(natout) { /* Use "dd if=/dev/random bs=2 count=1 | od -x" to reproduce */ @@ -372,6 +538,10 @@ ATF_TP_ADD_TCS(natout) ATF_TP_ADD_TC(natout, 6_cleartable); ATF_TP_ADD_TC(natout, 7_stress); ATF_TP_ADD_TC(natout, 8_portrange); + ATF_TP_ADD_TC(natout, 9_udp_eim_mapping); + ATF_TP_ADD_TC(natout, 10_udp_eim_out_in); + ATF_TP_ADD_TC(natout, 11_udp_eim_with_deny_incoming); + ATF_TP_ADD_TC(natout, 12_udp_eim_hairpinning); return atf_no_error(); } diff --git a/tests/sys/netinet/libalias/Makefile b/tests/sys/netinet/libalias/Makefile index 20d904511538..43bef996fae7 100644 --- a/tests/sys/netinet/libalias/Makefile +++ b/tests/sys/netinet/libalias/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netinet/libalias diff --git a/tests/sys/netinet/libalias/util.c b/tests/sys/netinet/libalias/util.c index 14ba196a59a5..8ceb8355c8ff 100644 --- a/tests/sys/netinet/libalias/util.c +++ b/tests/sys/netinet/libalias/util.c @@ -41,6 +41,7 @@ /* common ip ranges */ struct in_addr masq = { htonl(0x01020304) }; struct in_addr pub = { htonl(0x0102dead) }; +struct in_addr pub2 = { htonl(0x0102beef) }; struct in_addr prv1 = { htonl(0x0a00dead) }; struct in_addr prv2 = { htonl(0xac10dead) }; struct in_addr prv3 = { htonl(0xc0a8dead) }; diff --git a/tests/sys/netinet/libalias/util.h b/tests/sys/netinet/libalias/util.h index 786e48e41f37..f58a1ad26248 100644 --- a/tests/sys/netinet/libalias/util.h +++ b/tests/sys/netinet/libalias/util.h @@ -41,7 +41,7 @@ #define _UTIL_H /* common ip ranges */ -extern struct in_addr masq, pub, prv1, prv2, prv3, cgn, ext, ANY_ADDR; +extern struct in_addr masq, pub, pub2, prv1, prv2, prv3, cgn, ext, ANY_ADDR; int randcmp(const void *a, const void *b); void hexdump(void *p, size_t len); diff --git a/tests/sys/netinet/multicast.sh b/tests/sys/netinet/multicast.sh new file mode 100644 index 000000000000..eb2b962dac70 --- /dev/null +++ b/tests/sys/netinet/multicast.sh @@ -0,0 +1,61 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org> +# +# 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. + +. $(atf_get_srcdir)/../common/vnet.subr + +# See regression fixed in baad45c9c12028964acd0b58096f3aaa0fb22859 +atf_test_case "IP_MULTICAST_IF" "cleanup" +IP_MULTICAST_IF_head() +{ + atf_set descr \ + 'sendto() for IP_MULTICAST_IF socket does not do routing lookup' + atf_set require.user root + +} + +IP_MULTICAST_IF_body() +{ + local epair mjail + + vnet_init + # The test doesn't use our half of epair + epair=$(vnet_mkepair) + vnet_mkjail mjail ${epair}a + jexec mjail ifconfig ${epair}a up + jexec mjail ifconfig ${epair}a 192.0.2.1/24 + atf_check -s exit:0 -o empty \ + jexec mjail $(atf_get_srcdir)/sendto-IP_MULTICAST_IF 192.0.2.1 +} + +IP_MULTICAST_IF_cleanup() +{ + vnet_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "IP_MULTICAST_IF" +} diff --git a/tests/sys/netinet/redirect.sh b/tests/sys/netinet/redirect.sh index 4a069d515bb0..ad5b562da57a 100755 --- a/tests/sys/netinet/redirect.sh +++ b/tests/sys/netinet/redirect.sh @@ -34,7 +34,7 @@ valid_redirect_head() { atf_set descr 'Test valid IPv4 redirect' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } valid_redirect_body() { diff --git a/tests/sys/netinet/sendto-IP_MULTICAST_IF.c b/tests/sys/netinet/sendto-IP_MULTICAST_IF.c new file mode 100644 index 000000000000..d478e4da0b3b --- /dev/null +++ b/tests/sys/netinet/sendto-IP_MULTICAST_IF.c @@ -0,0 +1,63 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org> + * + * 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. + */ + +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <assert.h> +#include <err.h> + +int +main(int argc, char *argv[]) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + }; + struct in_addr in; + int s, rv; + + if (argc < 2) + errx(1, "Usage: %s IPv4-address", argv[0]); + + if (inet_pton(AF_INET, argv[1], &in) != 1) + err(1, "inet_pton(%s) failed", argv[1]); + + assert((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + assert(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0); + assert(setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &in, sizeof(in)) + == 0); + /* RFC 6676 */ + assert(inet_pton(AF_INET, "233.252.0.1", &sin.sin_addr) == 1); + sin.sin_port = htons(6676); + rv = sendto(s, &sin, sizeof(sin), 0, + (struct sockaddr *)&sin, sizeof(sin)); + if (rv != sizeof(sin)) + err(1, "sendto failed"); + + return (0); +} diff --git a/tests/sys/netinet/so_reuseport_lb_test.c b/tests/sys/netinet/so_reuseport_lb_test.c index 64fe0b53618d..fa9d6e425884 100644 --- a/tests/sys/netinet/so_reuseport_lb_test.c +++ b/tests/sys/netinet/so_reuseport_lb_test.c @@ -28,12 +28,16 @@ */ #include <sys/param.h> +#include <sys/event.h> #include <sys/socket.h> #include <netinet/in.h> +#include <netinet/tcp.h> #include <err.h> #include <errno.h> +#include <pthread.h> +#include <stdatomic.h> #include <stdlib.h> #include <unistd.h> @@ -235,10 +239,328 @@ ATF_TC_BODY(basic_ipv6, tc) } } +struct concurrent_add_softc { + struct sockaddr_storage ss; + int socks[128]; + int kq; +}; + +static void * +listener(void *arg) +{ + for (struct concurrent_add_softc *sc = arg;;) { + struct kevent kev; + ssize_t n; + int error, count, cs, s; + uint8_t b; + + count = kevent(sc->kq, NULL, 0, &kev, 1, NULL); + ATF_REQUIRE_MSG(count == 1, + "kevent() failed: %s", strerror(errno)); + + s = (int)kev.ident; + cs = accept(s, NULL, NULL); + ATF_REQUIRE_MSG(cs >= 0, + "accept() failed: %s", strerror(errno)); + + b = 'M'; + n = write(cs, &b, sizeof(b)); + ATF_REQUIRE_MSG(n >= 0, "write() failed: %s", strerror(errno)); + ATF_REQUIRE(n == 1); + + error = close(cs); + ATF_REQUIRE_MSG(error == 0 || errno == ECONNRESET, + "close() failed: %s", strerror(errno)); + } +} + +static void * +connector(void *arg) +{ + for (struct concurrent_add_softc *sc = arg;;) { + ssize_t n; + int error, s; + uint8_t b; + + s = socket(sc->ss.ss_family, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(s >= 0, "socket() failed: %s", strerror(errno)); + + error = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (int[]){1}, + sizeof(int)); + + error = connect(s, (struct sockaddr *)&sc->ss, sc->ss.ss_len); + ATF_REQUIRE_MSG(error == 0, "connect() failed: %s", + strerror(errno)); + + n = read(s, &b, sizeof(b)); + ATF_REQUIRE_MSG(n >= 0, "read() failed: %s", + strerror(errno)); + ATF_REQUIRE(n == 1); + ATF_REQUIRE(b == 'M'); + error = close(s); + ATF_REQUIRE_MSG(error == 0, + "close() failed: %s", strerror(errno)); + } +} + +/* + * Run three threads. One accepts connections from listening sockets on a + * kqueue, while the other makes connections. The third thread slowly adds + * sockets to the LB group. This is meant to help flush out race conditions. + */ +ATF_TC_WITHOUT_HEAD(concurrent_add); +ATF_TC_BODY(concurrent_add, tc) +{ + struct concurrent_add_softc sc; + struct sockaddr_in *sin; + pthread_t threads[4]; + int error; + + sc.kq = kqueue(); + ATF_REQUIRE_MSG(sc.kq >= 0, "kqueue() failed: %s", strerror(errno)); + + error = pthread_create(&threads[0], NULL, listener, &sc); + ATF_REQUIRE_MSG(error == 0, "pthread_create() failed: %s", + strerror(error)); + + sin = (struct sockaddr_in *)&sc.ss; + memset(sin, 0, sizeof(*sin)); + sin->sin_len = sizeof(*sin); + sin->sin_family = AF_INET; + sin->sin_port = htons(0); + sin->sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + for (size_t i = 0; i < nitems(sc.socks); i++) { + struct kevent kev; + int s; + + sc.socks[i] = s = socket(AF_INET, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(s >= 0, "socket() failed: %s", strerror(errno)); + + error = setsockopt(s, SOL_SOCKET, SO_REUSEPORT_LB, (int[]){1}, + sizeof(int)); + ATF_REQUIRE_MSG(error == 0, + "setsockopt(SO_REUSEPORT_LB) failed: %s", strerror(errno)); + + error = bind(s, (struct sockaddr *)sin, sizeof(*sin)); + ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", + strerror(errno)); + + error = listen(s, 5); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", + strerror(errno)); + + EV_SET(&kev, s, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0); + error = kevent(sc.kq, &kev, 1, NULL, 0, NULL); + ATF_REQUIRE_MSG(error == 0, "kevent() failed: %s", + strerror(errno)); + + if (i == 0) { + socklen_t slen = sizeof(sc.ss); + + error = getsockname(sc.socks[i], + (struct sockaddr *)&sc.ss, &slen); + ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s", + strerror(errno)); + ATF_REQUIRE(sc.ss.ss_family == AF_INET); + + for (size_t j = 1; j < nitems(threads); j++) { + error = pthread_create(&threads[j], NULL, + connector, &sc); + ATF_REQUIRE_MSG(error == 0, + "pthread_create() failed: %s", + strerror(error)); + } + } + + usleep(20000); + } +} + +/* + * Try calling listen(2) twice on a socket with SO_REUSEPORT_LB set. + */ +ATF_TC_WITHOUT_HEAD(double_listen_ipv4); +ATF_TC_BODY(double_listen_ipv4, tc) +{ + struct sockaddr_in sin; + int error, s; + + s = lb_listen_socket(PF_INET, 0); + + memset(&sin, 0, sizeof(sin)); + sin.sin_len = sizeof(sin); + sin.sin_family = AF_INET; + sin.sin_port = htons(0); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + error = bind(s, (struct sockaddr *)&sin, sizeof(sin)); + ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno)); + + error = listen(s, 1); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno)); + error = listen(s, 2); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno)); + + error = close(s); + ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno)); +} + +/* + * Try calling listen(2) twice on a socket with SO_REUSEPORT_LB set. + */ +ATF_TC_WITHOUT_HEAD(double_listen_ipv6); +ATF_TC_BODY(double_listen_ipv6, tc) +{ + struct sockaddr_in6 sin6; + int error, s; + + s = lb_listen_socket(PF_INET6, 0); + + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_len = sizeof(sin6); + sin6.sin6_family = AF_INET6; + sin6.sin6_port = htons(0); + sin6.sin6_addr = in6addr_loopback; + error = bind(s, (struct sockaddr *)&sin6, sizeof(sin6)); + ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno)); + + error = listen(s, 1); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno)); + error = listen(s, 2); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno)); + + error = close(s); + ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno)); +} + +/* + * Try binding many sockets to the same lbgroup without calling listen(2) on + * them. + */ +ATF_TC_WITHOUT_HEAD(bind_without_listen); +ATF_TC_BODY(bind_without_listen, tc) +{ + const int nsockets = 100; + struct sockaddr_in sin; + socklen_t socklen; + int error, s, s2[nsockets]; + + s = lb_listen_socket(PF_INET, 0); + + memset(&sin, 0, sizeof(sin)); + sin.sin_len = sizeof(sin); + sin.sin_family = AF_INET; + sin.sin_port = htons(0); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + error = bind(s, (struct sockaddr *)&sin, sizeof(sin)); + ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno)); + + socklen = sizeof(sin); + error = getsockname(s, (struct sockaddr *)&sin, &socklen); + ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s", + strerror(errno)); + + for (int i = 0; i < nsockets; i++) { + s2[i] = lb_listen_socket(PF_INET, 0); + error = bind(s2[i], (struct sockaddr *)&sin, sizeof(sin)); + ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno)); + } + for (int i = 0; i < nsockets; i++) { + error = listen(s2[i], 1); + ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno)); + } + for (int i = 0; i < nsockets; i++) { + error = close(s2[i]); + ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno)); + } + + error = close(s); + ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno)); +} + +/* + * Check that SO_REUSEPORT_LB doesn't mess with connect(2). + * Two sockets: + * 1) auxiliary peer socket 'p', where we connect to + * 2) test socket 's', that sets SO_REUSEPORT_LB and then connect(2)s to 'p' + */ +ATF_TC_WITHOUT_HEAD(connect_not_bound); +ATF_TC_BODY(connect_not_bound, tc) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(sin), + .sin_addr = { htonl(INADDR_LOOPBACK) }, + }; + socklen_t slen = sizeof(struct sockaddr_in); + int p, s, rv; + + ATF_REQUIRE((p = socket(PF_INET, SOCK_STREAM, 0)) > 0); + ATF_REQUIRE(bind(p, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(listen(p, 1) == 0); + ATF_REQUIRE(getsockname(p, (struct sockaddr *)&sin, &slen) == 0); + + s = lb_listen_socket(PF_INET, 0); + rv = connect(s, (struct sockaddr *)&sin, sizeof(sin)); + ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP, + "Expected EOPNOTSUPP on connect(2) not met. Got %d, errno %d", + rv, errno); + rv = sendto(s, "test", 4, 0, (struct sockaddr *)&sin, + sizeof(sin)); + ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP, + "Expected EOPNOTSUPP on sendto(2) not met. Got %d, errno %d", + rv, errno); + + close(p); + close(s); +} + +/* + * Same as above, but we also bind(2) between setsockopt(2) of SO_REUSEPORT_LB + * and the connect(2). + */ +ATF_TC_WITHOUT_HEAD(connect_bound); +ATF_TC_BODY(connect_bound, tc) +{ + struct sockaddr_in sin = { + .sin_family = AF_INET, + .sin_len = sizeof(sin), + .sin_addr = { htonl(INADDR_LOOPBACK) }, + }; + socklen_t slen = sizeof(struct sockaddr_in); + int p, s, rv; + + ATF_REQUIRE((p = socket(PF_INET, SOCK_STREAM, 0)) > 0); + ATF_REQUIRE(bind(p, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(listen(p, 1) == 0); + + s = lb_listen_socket(PF_INET, 0); + ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0); + ATF_REQUIRE(getsockname(p, (struct sockaddr *)&sin, &slen) == 0); + rv = connect(s, (struct sockaddr *)&sin, sizeof(sin)); + ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP, + "Expected EOPNOTSUPP on connect(2) not met. Got %d, errno %d", + rv, errno); + rv = sendto(s, "test", 4, 0, (struct sockaddr *)&sin, + sizeof(sin)); + ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP, + "Expected EOPNOTSUPP on sendto(2) not met. Got %d, errno %d", + rv, errno); + + close(p); + close(s); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, basic_ipv4); ATF_TP_ADD_TC(tp, basic_ipv6); + ATF_TP_ADD_TC(tp, concurrent_add); + ATF_TP_ADD_TC(tp, double_listen_ipv4); + ATF_TP_ADD_TC(tp, double_listen_ipv6); + ATF_TP_ADD_TC(tp, bind_without_listen); + ATF_TP_ADD_TC(tp, connect_not_bound); + ATF_TP_ADD_TC(tp, connect_bound); return (atf_no_error()); } diff --git a/tests/sys/netinet/socket_afinet.c b/tests/sys/netinet/socket_afinet.c index 7076f084719a..9c718fc5a901 100644 --- a/tests/sys/netinet/socket_afinet.c +++ b/tests/sys/netinet/socket_afinet.c @@ -2,6 +2,7 @@ * SPDX-License-Identifier: BSD-2-Clause * * Copyright (c) 2019 Bjoern A. Zeeb + * Copyright (c) 2024 Stormshield * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions @@ -25,11 +26,17 @@ * SUCH DAMAGE. */ -#include <sys/cdefs.h> +#include <sys/param.h> #include <sys/socket.h> +#include <sys/wait.h> + #include <netinet/in.h> + #include <errno.h> #include <poll.h> +#include <pwd.h> +#include <stdio.h> +#include <unistd.h> #include <atf-c.h> @@ -281,6 +288,301 @@ ATF_TC_BODY(socket_afinet_stream_reconnect, tc) ATF_CHECK_EQ(0, rc); } +/* + * Make sure that unprivileged users can't set the IP_BINDANY or IPV6_BINDANY + * socket options. + */ +ATF_TC(socket_afinet_bindany); +ATF_TC_HEAD(socket_afinet_bindany, tc) +{ + atf_tc_set_md_var(tc, "require.user", "unprivileged"); +} +ATF_TC_BODY(socket_afinet_bindany, tc) +{ + int s; + + s = socket(AF_INET, SOCK_STREAM, 0); + ATF_REQUIRE(s >= 0); + ATF_REQUIRE_ERRNO(EPERM, + setsockopt(s, IPPROTO_IP, IP_BINDANY, &(int){1}, sizeof(int)) == + -1); + ATF_REQUIRE(close(s) == 0); + + s = socket(AF_INET, SOCK_DGRAM, 0); + ATF_REQUIRE(s >= 0); + ATF_REQUIRE_ERRNO(EPERM, + setsockopt(s, IPPROTO_IP, IP_BINDANY, &(int){1}, sizeof(int)) == + -1); + ATF_REQUIRE(close(s) == 0); + + s = socket(AF_INET6, SOCK_STREAM, 0); + ATF_REQUIRE(s >= 0); + ATF_REQUIRE_ERRNO(EPERM, + setsockopt(s, IPPROTO_IPV6, IPV6_BINDANY, &(int){1}, sizeof(int)) == + -1); + ATF_REQUIRE(close(s) == 0); + + s = socket(AF_INET6, SOCK_DGRAM, 0); + ATF_REQUIRE(s >= 0); + ATF_REQUIRE_ERRNO(EPERM, + setsockopt(s, IPPROTO_IPV6, IPV6_BINDANY, &(int){1}, sizeof(int)) == + -1); + ATF_REQUIRE(close(s) == 0); +} + +/* + * Bind a socket to the specified address, optionally dropping privileges and + * setting one of the SO_REUSE* options first. + * + * Returns true if the bind succeeded, and false if it failed with EADDRINUSE. + */ +static bool +child_bind(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt, + bool unpriv) +{ + const char *user; + pid_t child; + + if (unpriv) { + if (!atf_tc_has_config_var(tc, "unprivileged_user")) + atf_tc_skip("unprivileged_user not set"); + user = atf_tc_get_config_var(tc, "unprivileged_user"); + } else { + user = NULL; + } + + child = fork(); + ATF_REQUIRE(child != -1); + if (child == 0) { + int s; + + if (user != NULL) { + struct passwd *passwd; + + passwd = getpwnam(user); + if (seteuid(passwd->pw_uid) != 0) + _exit(1); + } + + s = socket(sa->sa_family, type, 0); + if (s < 0) + _exit(2); + if (bind(s, sa, sa->sa_len) == 0) + _exit(3); + if (errno != EADDRINUSE) + _exit(4); + if (opt != 0) { + if (setsockopt(s, SOL_SOCKET, opt, &(int){1}, + sizeof(int)) != 0) + _exit(5); + } + if (bind(s, sa, sa->sa_len) == 0) + _exit(6); + if (errno != EADDRINUSE) + _exit(7); + _exit(0); + } else { + int status; + + ATF_REQUIRE_EQ(waitpid(child, &status, 0), child); + ATF_REQUIRE(WIFEXITED(status)); + status = WEXITSTATUS(status); + ATF_REQUIRE_MSG(status == 0 || status == 6, + "child exited with %d", status); + return (status == 6); + } +} + +static bool +child_bind_priv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt) +{ + return (child_bind(tc, type, sa, opt, false)); +} + +static bool +child_bind_unpriv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt) +{ + return (child_bind(tc, type, sa, opt, true)); +} + +static int +bind_socket(int domain, int type, int opt, bool unspec, struct sockaddr *sa) +{ + socklen_t slen; + int s; + + s = socket(domain, type, 0); + ATF_REQUIRE(s >= 0); + + if (domain == AF_INET) { + struct sockaddr_in sin; + + bzero(&sin, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_addr.s_addr = htonl(unspec ? + INADDR_ANY : INADDR_LOOPBACK); + sin.sin_port = htons(0); + ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0); + + slen = sizeof(sin); + } else /* if (domain == AF_INET6) */ { + struct sockaddr_in6 sin6; + + bzero(&sin6, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_addr = unspec ? in6addr_any : in6addr_loopback; + sin6.sin6_port = htons(0); + ATF_REQUIRE(bind(s, (struct sockaddr *)&sin6, sizeof(sin6)) == 0); + + slen = sizeof(sin6); + } + + if (opt != 0) { + ATF_REQUIRE(setsockopt(s, SOL_SOCKET, opt, &(int){1}, + sizeof(int)) == 0); + } + + ATF_REQUIRE(getsockname(s, sa, &slen) == 0); + + return (s); +} + +static void +multibind_test(const atf_tc_t *tc, int domain, int type) +{ + struct sockaddr_storage ss; + int opts[4] = { 0, SO_REUSEADDR, SO_REUSEPORT, SO_REUSEPORT_LB }; + int s; + bool flags[2] = { false, true }; + bool res; + + for (size_t flagi = 0; flagi < nitems(flags); flagi++) { + for (size_t opti = 0; opti < nitems(opts); opti++) { + s = bind_socket(domain, type, opts[opti], flags[flagi], + (struct sockaddr *)&ss); + for (size_t optj = 0; optj < nitems(opts); optj++) { + int opt; + + opt = opts[optj]; + res = child_bind_priv(tc, type, + (struct sockaddr *)&ss, opt); + /* + * Multi-binding is only allowed when both + * sockets have SO_REUSEPORT or SO_REUSEPORT_LB + * set. + */ + if (opts[opti] != 0 && + opts[opti] != SO_REUSEADDR && opti == optj) + ATF_REQUIRE(res); + else + ATF_REQUIRE(!res); + + res = child_bind_unpriv(tc, type, + (struct sockaddr *)&ss, opt); + /* + * Multi-binding is only allowed when both + * sockets have the same owner. + */ + ATF_REQUIRE(!res); + } + ATF_REQUIRE(close(s) == 0); + } + } +} + +/* + * Try to bind two sockets to the same address/port tuple. Under some + * conditions this is permitted. + */ +ATF_TC(socket_afinet_multibind); +ATF_TC_HEAD(socket_afinet_multibind, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); + atf_tc_set_md_var(tc, "require.config", "unprivileged_user"); +} +ATF_TC_BODY(socket_afinet_multibind, tc) +{ + multibind_test(tc, AF_INET, SOCK_STREAM); + multibind_test(tc, AF_INET, SOCK_DGRAM); + multibind_test(tc, AF_INET6, SOCK_STREAM); + multibind_test(tc, AF_INET6, SOCK_DGRAM); +} + +static void +bind_connected_port_test(const atf_tc_t *tc, int domain) +{ + struct sockaddr_in sin; + struct sockaddr_in6 sin6; + struct sockaddr *sinp; + int error, sd[3], tmp; + bool res; + + /* + * Create a connected socket pair. + */ + sd[0] = socket(domain, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd[0] >= 0, "socket failed: %s", strerror(errno)); + sd[1] = socket(domain, SOCK_STREAM, 0); + ATF_REQUIRE_MSG(sd[1] >= 0, "socket failed: %s", strerror(errno)); + if (domain == PF_INET) { + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_len = sizeof(sin); + sin.sin_addr.s_addr = htonl(INADDR_ANY); + sin.sin_port = htons(0); + sinp = (struct sockaddr *)&sin; + } else { + ATF_REQUIRE(domain == PF_INET6); + memset(&sin6, 0, sizeof(sin6)); + sin6.sin6_family = AF_INET6; + sin6.sin6_len = sizeof(sin6); + sin6.sin6_addr = in6addr_any; + sin6.sin6_port = htons(0); + sinp = (struct sockaddr *)&sin6; + } + + error = bind(sd[0], sinp, sinp->sa_len); + ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno)); + error = listen(sd[0], 1); + ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno)); + + error = getsockname(sd[0], sinp, &(socklen_t){ sinp->sa_len }); + ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno)); + if (domain == PF_INET) + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + error = connect(sd[1], sinp, sinp->sa_len); + ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno)); + tmp = accept(sd[0], NULL, NULL); + ATF_REQUIRE_MSG(tmp >= 0, "accept failed: %s", strerror(errno)); + ATF_REQUIRE(close(sd[0]) == 0); + sd[0] = tmp; + + /* bind() should succeed even from an unprivileged user. */ + res = child_bind(tc, SOCK_STREAM, sinp, 0, false); + ATF_REQUIRE(!res); + res = child_bind(tc, SOCK_STREAM, sinp, 0, true); + ATF_REQUIRE(!res); +} + +/* + * Normally bind() prevents port stealing by a different user, even when + * SO_REUSE* are specified. However, if the port is bound by a connected + * socket, then it's fair game. + */ +ATF_TC(socket_afinet_bind_connected_port); +ATF_TC_HEAD(socket_afinet_bind_connected_port, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); + atf_tc_set_md_var(tc, "require.config", "unprivileged_user"); +} +ATF_TC_BODY(socket_afinet_bind_connected_port, tc) +{ + bind_connected_port_test(tc, AF_INET); + bind_connected_port_test(tc, AF_INET6); +} + ATF_TP_ADD_TCS(tp) { ATF_TP_ADD_TC(tp, socket_afinet); @@ -289,6 +591,9 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, socket_afinet_poll_no_rdhup); ATF_TP_ADD_TC(tp, socket_afinet_poll_rdhup); ATF_TP_ADD_TC(tp, socket_afinet_stream_reconnect); + ATF_TP_ADD_TC(tp, socket_afinet_bindany); + ATF_TP_ADD_TC(tp, socket_afinet_multibind); + ATF_TP_ADD_TC(tp, socket_afinet_bind_connected_port); return atf_no_error(); } diff --git a/tests/sys/netinet/tcp_implied_connect.c b/tests/sys/netinet/tcp_implied_connect.c index 6e8cb0606a0a..d03d6be4fb92 100644 --- a/tests/sys/netinet/tcp_implied_connect.c +++ b/tests/sys/netinet/tcp_implied_connect.c @@ -51,6 +51,7 @@ ATF_TC_BODY(tcp_implied_connect, tc) ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0); len = sizeof(sin); ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &len) == 0); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ATF_REQUIRE(listen(s, -1) == 0); #if 0 /* diff --git a/tests/sys/netinet/udp_bindings.c b/tests/sys/netinet/udp_bindings.c new file mode 100644 index 000000000000..b05967d4b080 --- /dev/null +++ b/tests/sys/netinet/udp_bindings.c @@ -0,0 +1,249 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org> + * + * 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. + */ + +#include <sys/param.h> +#include <sys/jail.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <errno.h> +#include <ifaddrs.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <atf-c.h> + +static const char buf[] = "Hello"; + +static void +sendtolocalhost(int s) +{ + struct sockaddr_in dst = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + .sin_addr = { htonl(INADDR_LOOPBACK) }, + .sin_port = htons(1638), + }; + + ATF_REQUIRE(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)&dst, + sizeof(dst)) == sizeof(buf)); +} + +/* + * Echo back to the sender its own address in payload. + */ +static void * +echo(void *arg) +{ + int s = *(int *)arg; + struct sockaddr_in sin; + socklen_t slen = sizeof(sin); + char rbuf[sizeof(buf)]; + + ATF_REQUIRE(recvfrom(s, &rbuf, sizeof(rbuf), 0, (struct sockaddr *)&sin, + &slen) == sizeof(rbuf)); + printf("Echo to %s:%u\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port)); + ATF_REQUIRE(sendto(s, &sin, sizeof(sin), 0, (struct sockaddr *)&sin, + sizeof(sin)) == sizeof(sin)); + return (NULL); +} + +/* + * Cycle through local addresses (normally there should be at least two + * different IPv4 ones), and communicate to the echo server checking both + * IP_SENDSRCADDR and IP_RECVDSTADDR. Use same cmsg buffer for both send + * and receive operation, this is a suggested in manual, given that + * IP_RECVDSTADDR == IP_SENDSRCADDR. + * At the setup phase check that IP_SENDSRCADDR doesn't work on unbound socket. + */ +ATF_TC_WITHOUT_HEAD(IP_SENDSRCADDR); +ATF_TC_BODY(IP_SENDSRCADDR, tc) +{ + struct sockaddr_in srv = { + .sin_family = AF_INET, + .sin_len = sizeof(struct sockaddr_in), + }, dst; + char cbuf[CMSG_SPACE(sizeof(struct in_addr))]; + struct iovec iov = { + .iov_base = __DECONST(char *, buf), + .iov_len = sizeof(buf), + }; + struct iovec riov = { + .iov_base = &dst, + .iov_len = sizeof(dst), + }; + struct msghdr msg = { + .msg_iov = &iov, + .msg_iovlen = 1, + .msg_name = &srv, + .msg_namelen = sizeof(srv), + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf), + }; + struct msghdr rmsg = { + .msg_iov = &riov, + .msg_iovlen = 1, + .msg_control = cbuf, + .msg_controllen = sizeof(cbuf), + }; + struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); + *cmsg = (struct cmsghdr) { + .cmsg_level = IPPROTO_IP, + .cmsg_type = IP_SENDSRCADDR, + .cmsg_len = CMSG_LEN(sizeof(struct in_addr)), + }; + socklen_t slen = sizeof(struct sockaddr_in); + struct ifaddrs *ifa0, *ifa; + pthread_t tid; + int s, e; + + /* First check that IP_SENDSRCADDR doesn't work on an unbound socket. */ + ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE_MSG(sendmsg(s, &msg, 0) == -1 && errno == EINVAL, + "sendmsg(.cmsg_type = IP_SENDSRCADDR), errno %d", errno); + + /* Bind to random ports both sender and echo server. */ + ATF_REQUIRE(bind(s, (struct sockaddr *)&srv, sizeof(srv)) == 0); + ATF_REQUIRE((e = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(bind(e, (struct sockaddr *)&srv, sizeof(srv)) == 0); + ATF_REQUIRE(getsockname(e, (struct sockaddr *)&srv, &slen) == 0); + srv.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + ATF_REQUIRE(getifaddrs(&ifa0) == 0); + for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) { + struct sockaddr_in src; + struct in_addr vrf; + + if (ifa->ifa_addr->sa_family != AF_INET) + continue; + memcpy(&src, ifa->ifa_addr, sizeof(src)); + printf("Sending from %s\n", inet_ntoa(src.sin_addr)); + ATF_REQUIRE(pthread_create(&tid, NULL, echo, &e) == 0); + memcpy(CMSG_DATA(cmsg), &src.sin_addr, sizeof(src.sin_addr)); + ATF_REQUIRE(sendmsg(s, &msg, 0) == sizeof(buf)); + ATF_REQUIRE(recvmsg(s, &rmsg, 0) == sizeof(struct sockaddr_in)); + memcpy(&vrf, CMSG_DATA(cmsg), sizeof(vrf)); + ATF_REQUIRE_MSG(dst.sin_addr.s_addr == src.sin_addr.s_addr, + "Sent from %s, but echo server reports %s", + inet_ntoa(src.sin_addr), inet_ntoa(dst.sin_addr)); + ATF_REQUIRE_MSG(vrf.s_addr == src.sin_addr.s_addr, + "Sent from %s, but IP_RECVDSTADDR reports %s", + inet_ntoa(src.sin_addr), inet_ntoa(vrf)); + ATF_REQUIRE(pthread_join(tid, NULL) == 0); + } + + freeifaddrs(ifa0); + close(s); + close(e); +} + +/* + * Check gethostname(2) on a newborn socket, and then on an unconnected, but + * used socket. The first shall return all-zeroes, and second one should + * return us our assigned port. + */ +ATF_TC_WITHOUT_HEAD(gethostname); +ATF_TC_BODY(gethostname, tc) +{ + struct sockaddr_in sin; + socklen_t slen = sizeof(sin); + int s; + + ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0); + ATF_REQUIRE_MSG(sin.sin_addr.s_addr == INADDR_ANY && sin.sin_port == 0, + "newborn socket name %s:%u", inet_ntoa(sin.sin_addr), + ntohs(sin.sin_port)); + sendtolocalhost(s); + ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0); + ATF_REQUIRE_MSG(sin.sin_addr.s_addr == INADDR_ANY && sin.sin_port != 0, + "used unconnected socket name %s:%u", inet_ntoa(sin.sin_addr), + ntohs(sin.sin_port)); + close(s); +} + +ATF_TC_WITHOUT_HEAD(gethostname_jailed); +ATF_TC_BODY(gethostname_jailed, tc) +{ + struct in_addr laddr = { htonl(INADDR_LOOPBACK) }; + struct jail jconf = { + .version = JAIL_API_VERSION, + .path = __DECONST(char *, "/"), + .hostname = __DECONST(char *,"test"), + .ip4s = 1, + .ip4 = &laddr, + }; + struct sockaddr_in sin; + socklen_t slen = sizeof(sin); + int s; + + ATF_REQUIRE(jail(&jconf) > 0); + ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0); + sendtolocalhost(s); + ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0); + ATF_REQUIRE_MSG(sin.sin_addr.s_addr == laddr.s_addr && + sin.sin_port != 0, + "jailed unconnected socket name %s:%u", inet_ntoa(sin.sin_addr), + ntohs(sin.sin_port)); + close(s); +} + +/* + * See bug 274009. + */ +ATF_TC_WITHOUT_HEAD(v4mapped); +ATF_TC_BODY(v4mapped, tc) +{ + struct sockaddr_in6 sa6 = { + .sin6_family = AF_INET6, + .sin6_len = sizeof(struct sockaddr_in6), + .sin6_port = htons(1), + }; + int s; + + ATF_REQUIRE((s = socket(PF_INET6, SOCK_DGRAM, 0)) > 0); + ATF_REQUIRE(setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &(int){0}, + sizeof(int)) == 0); + ATF_REQUIRE(inet_pton(AF_INET6, "::ffff:127.0.0.1", &(sa6.sin6_addr)) + == 1); + ATF_REQUIRE(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)&sa6, + sizeof(sa6)) == sizeof(buf)); + close(s); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, v4mapped); + ATF_TP_ADD_TC(tp, gethostname); + ATF_TP_ADD_TC(tp, gethostname_jailed); + ATF_TP_ADD_TC(tp, IP_SENDSRCADDR); + + return (atf_no_error()); +} diff --git a/tests/sys/netinet/udp_io.c b/tests/sys/netinet/udp_io.c index 27cd02735ed4..04f9bf56ed02 100644 --- a/tests/sys/netinet/udp_io.c +++ b/tests/sys/netinet/udp_io.c @@ -52,6 +52,7 @@ udp_socketpair(int *s) ATF_REQUIRE((c = socket(PF_INET, SOCK_DGRAM, 0)) > 0); ATF_REQUIRE(bind(b, (struct sockaddr *)&sin, sizeof(sin)) == 0); ATF_REQUIRE(getsockname(b, (struct sockaddr *)&sin, &slen) == 0); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); ATF_REQUIRE(connect(c, (struct sockaddr *)&sin, sizeof(sin)) == 0); s[0] = b; diff --git a/tests/sys/netinet6/Makefile b/tests/sys/netinet6/Makefile index 82e84859ecbc..26f1a18a8d32 100644 --- a/tests/sys/netinet6/Makefile +++ b/tests/sys/netinet6/Makefile @@ -1,32 +1,52 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netinet6 FILESDIR= ${TESTSDIR} ATF_TESTS_PYTEST= test_ip6_output.py -ATF_TESTS_SH= \ - exthdr \ - mld \ - scapyi386 \ - redirect \ - divert \ - forward6 \ - output6 \ - lpm6 \ - fibs6 \ - ndp \ - proxy_ndp -TEST_METADATA.output6+= required_programs="python" +ATF_TESTS_SH= exthdr \ + mld \ + scapyi386 \ + redirect \ + divert \ + forward6 \ + output6 \ + lpm6 \ + fibs6 \ + ndp \ + proxy_ndp \ + addr6 + +TEST_METADATA.divert+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.exthdr+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.forward6+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.ndp+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.output6+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" \ + required_programs="python" +TEST_METADATA.proxy_ndp+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.redirect+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.scapyi386+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" +TEST_METADATA.addr6+= execenv="jail" \ + execenv_jail_params="vnet allow.raw_sockets" -${PACKAGE}FILES+= exthdr.py -${PACKAGE}FILES+= mld.py -${PACKAGE}FILES+= scapyi386.py -${PACKAGE}FILES+= redirect.py +${PACKAGE}FILES+= exthdr.py \ + mld.py \ + scapyi386.py \ + ra.py \ + redirect.py ${PACKAGE}FILESMODE_exthdr.py= 0555 ${PACKAGE}FILESMODE_mld.py= 0555 ${PACKAGE}FILESMODE_scapyi386.py=0555 +${PACKAGE}FILESMODE_ra.py=0555 ${PACKAGE}FILESMODE_redirect.py=0555 TESTS_SUBDIRS+= frag6 diff --git a/tests/sys/netinet6/addr6.sh b/tests/sys/netinet6/addr6.sh new file mode 100755 index 000000000000..38e4bb152240 --- /dev/null +++ b/tests/sys/netinet6/addr6.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env atf-sh +#- +# SPDX-License-Identifier: ISC +# +# Copyright (c) 2025 Lexi Winter. +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +. $(atf_get_srcdir)/../common/vnet.subr + +atf_test_case "addr6_invalid_addr" "cleanup" +addr6_invalid_addr_head() +{ + atf_set descr "adding an invalid IPv6 address returns an error" + atf_set require.user root +} + +addr6_invalid_addr_body() +{ + vnet_init + + ep=$(vnet_mkepair) + atf_check -s exit:0 ifconfig ${ep}a inet6 2001:db8::1/128 + atf_check -s exit:1 -e ignore ifconfig ${ep}a inet6 2001:db8::1/127 alias +} + +addr6_invalid_addr_cleanup() +{ + vnet_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "addr6_invalid_addr" +} diff --git a/tests/sys/netinet6/divert.sh b/tests/sys/netinet6/divert.sh index 2cf57c5966b0..e2dc3e26d97e 100755 --- a/tests/sys/netinet6/divert.sh +++ b/tests/sys/netinet6/divert.sh @@ -41,11 +41,10 @@ ipdivert_ip6_output_remote_success_head() { atf_set descr 'Test valid IPv6 redirect' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } ipdivert_ip6_output_remote_success_body() { - ids=65530 id=`printf "%x" ${ids}` if [ $$ -gt 65535 ]; then diff --git a/tests/sys/netinet6/exthdr.sh b/tests/sys/netinet6/exthdr.sh index 350307f13eae..3d866d85ea83 100755 --- a/tests/sys/netinet6/exthdr.sh +++ b/tests/sys/netinet6/exthdr.sh @@ -32,7 +32,7 @@ exthdr_head() { atf_set descr 'Test IPv6 extension header code paths' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } exthdr_body() { diff --git a/tests/sys/netinet6/forward6.sh b/tests/sys/netinet6/forward6.sh index b3ccd30aea62..e4b027bf281a 100755 --- a/tests/sys/netinet6/forward6.sh +++ b/tests/sys/netinet6/forward6.sh @@ -34,7 +34,7 @@ fwd_ip6_gu_icmp_iface_fast_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to interface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_iface_fast_success_body() { @@ -104,7 +104,7 @@ fwd_ip6_gu_icmp_gw_gu_fast_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to GU gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_gw_gu_fast_success_body() { @@ -178,7 +178,7 @@ fwd_ip6_gu_icmp_gw_ll_fast_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to LL gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_gw_ll_fast_success_body() { @@ -253,7 +253,7 @@ fwd_ip6_gu_icmp_iface_slow_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to interface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_iface_slow_success_body() { @@ -322,7 +322,7 @@ fwd_ip6_gu_icmp_gw_gu_slow_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to GU gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_gw_gu_slow_success_body() { @@ -397,7 +397,7 @@ fwd_ip6_gu_icmp_gw_ll_slow_success_head() { atf_set descr 'Test valid IPv6 global unicast fast-forwarding to LL gw' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } fwd_ip6_gu_icmp_gw_ll_slow_success_body() { @@ -466,6 +466,59 @@ fwd_ip6_gu_icmp_gw_ll_slow_success_cleanup() { vnet_cleanup } +atf_test_case "fwd_ip6_blackhole" "cleanup" +fwd_ip6_blackhole_head() { + + atf_set descr 'Test blackhole routing' + atf_set require.user root +} + +fwd_ip6_blackhole_body() { + jname="v6t-fwd_ip6_blackhole" + + vnet_init + + epair=$(vnet_mkepair) + epair_out=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + + vnet_mkjail ${jname} ${epair}b ${epair_out}b + jexec ${jname} ifconfig lo0 inet6 ::1/128 up no_dad + jexec ${jname} ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec ${jname} ifconfig ${epair_out}b inet6 2001:db8:1::1/64 up no_dad + jexec ${jname} sysctl net.inet6.ip6.forwarding=1 + + route -6 add default 2001:db8::1 + + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -t 1 2001:db8:1::2 + atf_check -s exit:0 -o match:"0 packets not forwardable" \ + jexec ${jname} netstat -s -p ip6 + + # Create blackhole route + jexec ${jname} route -6 add 2001:db8:1::2 -blackhole + + # Force slow path + jexec ${jname} sysctl net.inet6.ip6.redirect=1 + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -t 1 2001:db8:1::2 + atf_check -s exit:0 -o match:"1 packet not forwardable" \ + jexec ${jname} netstat -s -p ip6 + + # Now try the fast path + jexec ${jname} sysctl net.inet6.ip6.redirect=0 + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -t 1 2001:db8:1::2 + atf_check -s exit:0 -o match:"2 packets not forwardable" \ + jexec ${jname} netstat -s -p ip6 +} + +fwd_ip6_blackhole_cleanup() { + + vnet_cleanup +} + atf_init_test_cases() { @@ -475,6 +528,7 @@ atf_init_test_cases() atf_add_test_case "fwd_ip6_gu_icmp_iface_slow_success" atf_add_test_case "fwd_ip6_gu_icmp_gw_gu_slow_success" atf_add_test_case "fwd_ip6_gu_icmp_gw_ll_slow_success" + atf_add_test_case "fwd_ip6_blackhole" } # end diff --git a/tests/sys/netinet6/frag6/Makefile b/tests/sys/netinet6/frag6/Makefile index d1661060368b..3fca0522e533 100644 --- a/tests/sys/netinet6/frag6/Makefile +++ b/tests/sys/netinet6/frag6/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netinet6/frag6 diff --git a/tests/sys/netinet6/frag6/frag6.subr b/tests/sys/netinet6/frag6/frag6.subr index 70e7386b60cc..238c9619c398 100644 --- a/tests/sys/netinet6/frag6/frag6.subr +++ b/tests/sys/netinet6/frag6/frag6.subr @@ -31,7 +31,7 @@ frag6_head() { atf_set descr 'Test IPv6 fragmentation code' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } frag6_body() diff --git a/tests/sys/netinet6/mld.sh b/tests/sys/netinet6/mld.sh index 7229aa34dc52..d98624daedf5 100755 --- a/tests/sys/netinet6/mld.sh +++ b/tests/sys/netinet6/mld.sh @@ -32,7 +32,7 @@ mldraw01_head() { atf_set descr 'Test for correct Ethernet Destination MAC address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } mldraw01_body() { diff --git a/tests/sys/netinet6/ndp.sh b/tests/sys/netinet6/ndp.sh index eddd49112421..bac9764ee3c9 100755 --- a/tests/sys/netinet6/ndp.sh +++ b/tests/sys/netinet6/ndp.sh @@ -25,7 +25,6 @@ # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # SUCH DAMAGE. # -# . $(atf_get_srcdir)/../common/vnet.subr @@ -36,6 +35,7 @@ ndp_add_gu_success_head() { } ndp_add_gu_success_body() { + local epair0 jname vnet_init @@ -74,6 +74,7 @@ ndp_del_gu_success_head() { } ndp_del_gu_success_body() { + local epair0 jname vnet_init @@ -102,13 +103,94 @@ ndp_del_gu_success_cleanup() { vnet_cleanup } +ndp_if_up() +{ + local ifname=$1 + local jname=$2 -atf_init_test_cases() + if [ -n "$jname" ]; then + jname="jexec ${jname}" + fi + atf_check ${jname} ifconfig ${ifname} up + atf_check ${jname} ifconfig ${ifname} inet6 -ifdisabled + while ${jname} ifconfig ${ifname} inet6 | grep tentative; do + sleep 0.1 + done +} + +ndp_if_lladdr() { + local ifname=$1 + local jname=$2 - atf_add_test_case "ndp_add_gu_success" - atf_add_test_case "ndp_del_gu_success" + if [ -n "$jname" ]; then + jname="jexec ${jname}" + fi + ${jname} ifconfig ${ifname} inet6 | \ + awk '/inet6 fe80:/{split($2, addr, "%"); print addr[1]}' } -# end +atf_test_case "ndp_slaac_default_route" "cleanup" +ndp_slaac_default_route_head() { + atf_set descr 'Test default route installation via SLAAC' + atf_set require.user root + atf_set require.progs python3 scapy +} + +ndp_slaac_default_route_body() { + local epair0 jname lladdr + + vnet_init + + jname="v6t-ndp_slaac_default_route" + + epair0=$(vnet_mkepair) + vnet_mkjail ${jname} ${epair0}a + + ndp_if_up ${epair0}a ${jname} + ndp_if_up ${epair0}b + atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv + + # Send an RA advertising a prefix. + atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \ + --sendif ${epair0}b \ + --dst $(ndp_if_lladdr ${epair0}a ${jname}) \ + --src $(ndp_if_lladdr ${epair0}b) \ + --prefix "2001:db8:ffff:1000::" --prefixlen 64 + + # Wait for a default router to appear. + while [ -z "$(jexec ${jname} ndp -r)" ]; do + sleep 0.1 + done + atf_check -o match:"^default[[:space:]]+fe80:" \ + jexec ${jname} netstat -rn -6 + + # Get rid of the default route. + jexec ${jname} route -6 flush + atf_check -o not-match:"^default[[:space:]]+fe80:" \ + jexec ${jname} netstat -rn -6 + + # Send another RA, make sure that the default route is installed again. + atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \ + --sendif ${epair0}b \ + --dst $(ndp_if_lladdr ${epair0}a ${jname}) \ + --src $(ndp_if_lladdr ${epair0}b) \ + --prefix "2001:db8:ffff:1000::" --prefixlen 64 + while [ -z "$(jexec ${jname} ndp -r)" ]; do + sleep 0.1 + done + atf_check -o match:"^default[[:space:]]+fe80:" \ + jexec ${jname} netstat -rn -6 +} + +ndp_slaac_default_route_cleanup() { + vnet_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "ndp_add_gu_success" + atf_add_test_case "ndp_del_gu_success" + atf_add_test_case "ndp_slaac_default_route" +} diff --git a/tests/sys/netinet6/ra.py b/tests/sys/netinet6/ra.py new file mode 100644 index 000000000000..44814418da48 --- /dev/null +++ b/tests/sys/netinet6/ra.py @@ -0,0 +1,38 @@ +#- +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Klara, Inc. +# + +import argparse +import scapy.all as sp +import sys + +# +# Emit a router advertisement with the specified prefix. +# +def main(): + parser = argparse.ArgumentParser("ra.py", + description="Emits Router Advertisement packets") + parser.add_argument('--sendif', nargs=1, required=True, + help='The interface through which the packet will be sent') + parser.add_argument('--src', nargs=1, required=True, + help='The source IP address') + parser.add_argument('--dst', nargs=1, required=True, + help='The destination IP address') + parser.add_argument('--prefix', nargs=1, required=True, + help='The prefix to be advertised') + parser.add_argument('--prefixlen', nargs=1, required=True, type=int, + help='The prefix length to be advertised') + + args = parser.parse_args() + pkt = sp.Ether() / \ + sp.IPv6(src=args.src, dst=args.dst) / \ + sp.ICMPv6ND_RA(chlim=64) / \ + sp.ICMPv6NDOptPrefixInfo(prefix=args.prefix, prefixlen=args.prefixlen) + + sp.sendp(pkt, iface=args.sendif[0], verbose=False) + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/tests/sys/netinet6/redirect.sh b/tests/sys/netinet6/redirect.sh index 64efe7339ffd..40874f8c9b6d 100644 --- a/tests/sys/netinet6/redirect.sh +++ b/tests/sys/netinet6/redirect.sh @@ -34,15 +34,11 @@ valid_redirect_head() { atf_set descr 'Test valid IPv6 redirect' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } valid_redirect_body() { - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/247729" - fi - ids=65533 id=`printf "%x" ${ids}` if [ $$ -gt 65535 ]; then @@ -89,7 +85,7 @@ valid_redirect_body() { while [ `ifconfig ${epair}a inet6 | grep -c tentative` != "0" ]; do sleep 0.1 done - while [ `jexec ${jname}b ifconfig ${epair}b inet6 | grep -c tentative` != "0" ]; do + while [ `jexec ${jname} ifconfig ${epair}b inet6 | grep -c tentative` != "0" ]; do sleep 0.1 done diff --git a/tests/sys/netinet6/scapyi386.sh b/tests/sys/netinet6/scapyi386.sh index bb9ee4cfcd2a..2d91f25dd01e 100755 --- a/tests/sys/netinet6/scapyi386.sh +++ b/tests/sys/netinet6/scapyi386.sh @@ -32,7 +32,7 @@ scapyi386_head() { atf_set descr 'Test for correct Ethernet Destination MAC address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } scapyi386_body() { diff --git a/tests/sys/netipsec/Makefile b/tests/sys/netipsec/Makefile index af5c0f75e8ba..bb10297e0cc3 100644 --- a/tests/sys/netipsec/Makefile +++ b/tests/sys/netipsec/Makefile @@ -1,4 +1,3 @@ - TESTSDIR= ${TESTSBASE}/sys/netipsec TESTS_SUBDIRS+= tunnel diff --git a/tests/sys/netipsec/tunnel/Makefile b/tests/sys/netipsec/tunnel/Makefile index c28826711cb8..c6060a790cc3 100644 --- a/tests/sys/netipsec/tunnel/Makefile +++ b/tests/sys/netipsec/tunnel/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netipsec/tunnel diff --git a/tests/sys/netipsec/tunnel/empty.sh b/tests/sys/netipsec/tunnel/empty.sh index dc1d3708f744..56480d21f4ec 100755 --- a/tests/sys/netipsec/tunnel/empty.sh +++ b/tests/sys/netipsec/tunnel/empty.sh @@ -11,7 +11,7 @@ v4_head() v4_body() { # Can't use filename "null" for this script: PR 223564 - ist_test 4 null "" + ist_test 4 null "1234" } v4_cleanup() @@ -28,7 +28,7 @@ v6_head() v6_body() { - ist_test 6 null "" + ist_test 6 null "5678" } v6_cleanup() diff --git a/tests/sys/netlink/Makefile b/tests/sys/netlink/Makefile index c52149674ceb..c07ef8663867 100644 --- a/tests/sys/netlink/Makefile +++ b/tests/sys/netlink/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests WARNS?= 1 diff --git a/tests/sys/netlink/test_rtnl_ifaddr.py b/tests/sys/netlink/test_rtnl_ifaddr.py index 48e53502df54..768bf38153ff 100644 --- a/tests/sys/netlink/test_rtnl_ifaddr.py +++ b/tests/sys/netlink/test_rtnl_ifaddr.py @@ -33,7 +33,10 @@ class TestRtNlIfaddrList(NetlinkTestTemplate, SingleVnetTestTemplate): def setup_method(self, method): method_name = method.__name__ if "4" in method_name: - self.IPV4_PREFIXES = ["192.0.2.1/24"] + if "nofilter" in method_name: + self.IPV4_PREFIXES = ["192.0.2.1/24", "169.254.169.254/16"] + else: + self.IPV4_PREFIXES = ["192.0.2.1/24"] if "6" in method_name: self.IPV6_PREFIXES = ["2001:db8::1/64"] super().setup_method(method) @@ -49,14 +52,21 @@ class TestRtNlIfaddrList(NetlinkTestTemplate, SingleVnetTestTemplate): for rx_msg in self.read_msg_list(msg.nl_hdr.nlmsg_seq, NlRtMsgType.RTM_NEWADDR): ifname = socket.if_indextoname(rx_msg.base_hdr.ifa_index) family = rx_msg.base_hdr.ifa_family - ret.append((ifname, family, rx_msg)) + scope = rx_msg.base_hdr.ifa_scope + ret.append((ifname, family, scope)) ifname = "lo0" - assert len([r for r in ret if r[0] == ifname]) > 0 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_HOST.value]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_HOST.value]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1 + assert len([r for r in ret if r[0] == ifname]) == 3 ifname = self.vnet.iface_alias_map["if1"].name - assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET]) == 1 - assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6]) == 2 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_UNIVERSE.value]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1 + assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_UNIVERSE.value]) == 1 + assert len([r for r in ret if r[0] == ifname]) == 4 def test_46_filter_iface(self): """Tests that listing outputs both IPv4/IPv6 for the specific interface""" diff --git a/tests/sys/netlink/test_snl.c b/tests/sys/netlink/test_snl.c index bd607efa82fc..040414a96e2c 100644 --- a/tests/sys/netlink/test_snl.c +++ b/tests/sys/netlink/test_snl.c @@ -13,6 +13,18 @@ #include <atf-c.h> +static const struct snl_hdr_parser *snl_all_core_parsers[] = { + &snl_errmsg_parser, &snl_donemsg_parser, + &_nla_bit_parser, &_nla_bitset_parser, +}; + +static const struct snl_hdr_parser *snl_all_route_parsers[] = { + &_metrics_mp_nh_parser, &_mpath_nh_parser, &_metrics_parser, &snl_rtm_route_parser, + &_link_fbsd_parser, &snl_rtm_link_parser, &snl_rtm_link_parser_simple, + &_neigh_fbsd_parser, &snl_rtm_neigh_parser, + &_addr_fbsd_parser, &snl_rtm_addr_parser, &_nh_fbsd_parser, &snl_nhmsg_parser, +}; + static void require_netlink(void) { diff --git a/tests/sys/netlink/test_snl_generic.c b/tests/sys/netlink/test_snl_generic.c index d84d3f88f487..c63b1380f2ad 100644 --- a/tests/sys/netlink/test_snl_generic.c +++ b/tests/sys/netlink/test_snl_generic.c @@ -11,6 +11,10 @@ #include <atf-c.h> +static const struct snl_hdr_parser *snl_all_genl_parsers[] = { + &_genl_ctrl_getfam_parser, &_genl_ctrl_mc_parser, +}; + static void require_netlink(void) { @@ -98,7 +102,7 @@ ATF_TC_BODY(test_snl_get_genl_family_groups, tc) ATF_CHECK(snl_parse_nlmsg(&ss, hdr, &_genl_ctrl_getfam_parser, &attrs)); ATF_CHECK_EQ(attrs.mcast_groups.num_groups, 1); - struct snl_genl_ctrl_mcast_group *group = attrs.mcast_groups.groups[0]; + struct _snl_genl_ctrl_mcast_group *group = attrs.mcast_groups.groups[0]; ATF_CHECK(group->mcast_grp_id > 0); ATF_CHECK(!strcmp(group->mcast_grp_name, "notify")); diff --git a/tests/sys/netmap/Makefile b/tests/sys/netmap/Makefile index 8c3ae26284ec..ee00d0421620 100644 --- a/tests/sys/netmap/Makefile +++ b/tests/sys/netmap/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netmap diff --git a/tests/sys/netmap/ctrl-api-test.c b/tests/sys/netmap/ctrl-api-test.c index 8d33b4c58d2a..6b45dbb1cfea 100644 --- a/tests/sys/netmap/ctrl-api-test.c +++ b/tests/sys/netmap/ctrl-api-test.c @@ -1596,6 +1596,7 @@ sync_kloop_csb_enable(struct TestContext *ctx) return sync_kloop_start_stop(ctx); } +#if 0 static int sync_kloop_conflict(struct TestContext *ctx) { @@ -1640,6 +1641,14 @@ sync_kloop_conflict(struct TestContext *ctx) /* Wait for one of the two threads to fail to start the kloop, to * avoid a race condition where th1 starts the loop and stops, * and after that th2 starts the loop successfully. */ + /* + * XXX: This doesn't fully close the race. th2 might fail to + * start executing since th1 can enter the kernel and hog the + * CPU on a single-CPU system until the semaphore timeout + * awakens this thread and it calls sync_kloop_stop. Once th1 + * exits the kernel, th2 can finally run and will then loop + * forever in the ioctl handler. + */ clock_gettime(CLOCK_REALTIME, &to); to.tv_sec += 2; ret = sem_timedwait(&sem, &to); @@ -1674,6 +1683,7 @@ sync_kloop_conflict(struct TestContext *ctx) ? 0 : -1; } +#endif static int sync_kloop_eventfds_mismatch(struct TestContext *ctx) @@ -2079,7 +2089,9 @@ static struct mytest tests[] = { decltest(sync_kloop_eventfds_all_direct_rx), decltest(sync_kloop_nocsb), decltest(sync_kloop_csb_enable), +#if 0 decltest(sync_kloop_conflict), +#endif decltest(sync_kloop_eventfds_mismatch), decltest(null_port), decltest(null_port_all_zero), diff --git a/tests/sys/netpfil/Makefile b/tests/sys/netpfil/Makefile index bdbb9078cfc2..b449902aabc2 100644 --- a/tests/sys/netpfil/Makefile +++ b/tests/sys/netpfil/Makefile @@ -1,4 +1,3 @@ - .include <src.opts.mk> TESTSDIR= ${TESTSBASE}/sys/netpfil diff --git a/tests/sys/netpfil/common/Makefile b/tests/sys/netpfil/common/Makefile index 0003aac28779..0938bd9d9c7e 100644 --- a/tests/sys/netpfil/common/Makefile +++ b/tests/sys/netpfil/common/Makefile @@ -1,7 +1,7 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/common +BINDIR= ${TESTSDIR} ATF_TESTS_SH+= \ @@ -13,8 +13,11 @@ ATF_TESTS_SH+= \ fragments \ forward -# Tests reuse jail names and so cannot run in parallel. -TEST_METADATA+= is_exclusive=true +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" + +PROGS= divapp ${PACKAGE}FILES+= \ utils.subr \ diff --git a/tests/sys/netpfil/pf/divapp.c b/tests/sys/netpfil/common/divapp.c index 908c41eaa67f..d0f4b345b14c 100644 --- a/tests/sys/netpfil/pf/divapp.c +++ b/tests/sys/netpfil/common/divapp.c @@ -25,7 +25,7 @@ * SUCH DAMAGE. */ -/* Used by tests like divert-to.sh */ +/* Used by divert(4) related tests */ #include <errno.h> #include <stdlib.h> @@ -83,8 +83,8 @@ recv_pkt(struct context *c) s = select(c->fd + 1, &readfds, 0, 0, &timeout); if (s == -1) errx(EX_IOERR, "recv_pkt: select() errors."); - if (s != 1) // timeout - return -1; + if (s != 1) /* timeout */ + return (-1); c->pkt_n = recvfrom(c->fd, c->pkt, sizeof(c->pkt), 0, (struct sockaddr *) &c->sin, &c->sin_len); @@ -98,14 +98,11 @@ static void send_pkt(struct context *c) { ssize_t n; - char errstr[32]; n = sendto(c->fd, c->pkt, c->pkt_n, 0, (struct sockaddr *) &c->sin, c->sin_len); - if (n == -1) { - strerror_r(errno, errstr, sizeof(errstr)); - errx(EX_IOERR, "send_pkt: sendto() errors: %d %s.", errno, errstr); - } + if (n == -1) + err(EX_IOERR, "send_pkt: sendto() errors"); if (n != c->pkt_n) errx(EX_IOERR, "send_pkt: sendto() sent %zd of %zd bytes.", n, c->pkt_n); @@ -145,5 +142,5 @@ main(int argc, char *argv[]) if (npkt != 1) errx(EXIT_FAILURE, "%d: npkt=%d.", c.divert_port, npkt); - return EXIT_SUCCESS; + return (EXIT_SUCCESS); } diff --git a/tests/sys/netpfil/common/dummynet.sh b/tests/sys/netpfil/common/dummynet.sh index 3c930cfe2aff..b77b2df84010 100644 --- a/tests/sys/netpfil/common/dummynet.sh +++ b/tests/sys/netpfil/common/dummynet.sh @@ -277,7 +277,7 @@ queue_body() ifconfig ${epair}a 192.0.2.1/24 up jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf # Sanity check @@ -320,7 +320,7 @@ queue_body() # TCP should still just pass fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c) if [ $result -ne 2048000 ]; @@ -329,7 +329,7 @@ queue_body() fails=$(( ${fails} + 1 )) fi done - if [ ${fails} -gt 0 ]; + if [ ${fails} -gt 2 ]; then atf_fail "We failed prioritisation ${fails} times" fi @@ -348,7 +348,7 @@ queue_body() sleep 1 fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c) if [ $result -ne 2048000 ]; @@ -385,7 +385,7 @@ queue_v6_body() ifconfig ${epair}a inet6 2001:db8:42::1/64 no_dad up jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2 no_dad up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf jexec alcatraz sysctl net.inet6.icmp6.errppslimit=0 @@ -429,7 +429,7 @@ queue_v6_body() # TCP should still just pass fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c) if [ $result -ne 1024000 ]; @@ -438,7 +438,7 @@ queue_v6_body() fails=$(( ${fails} + 1 )) fi done - if [ ${fails} -gt 0 ]; + if [ ${fails} -gt 2 ]; then atf_fail "We failed prioritisation ${fails} times" fi @@ -454,7 +454,7 @@ queue_v6_body() "pass in proto icmp6 dnqueue (0, 100)" fails=0 - for i in `seq 1 3` + for i in `seq 1 5` do result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c) if [ $result -ne 1024000 ]; @@ -557,7 +557,7 @@ pls_basic_body() # are dropped (84 - 96 responses). # repeat up to 6 times if the initial # checks fail - atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2 + atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2 } pls_basic_cleanup() @@ -604,7 +604,7 @@ pls_gilbert_body() # are dropped (70 - 85 responses). # repeat up to 6 times if the initial # checks fail - atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2 + atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2 } pls_gilbert_cleanup() diff --git a/tests/sys/netpfil/common/forward.sh b/tests/sys/netpfil/common/forward.sh index 939ce55f2d33..fa1f97aa0390 100644 --- a/tests/sys/netpfil/common/forward.sh +++ b/tests/sys/netpfil/common/forward.sh @@ -33,7 +33,7 @@ v4_head() { atf_set descr 'Basic forwarding test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() diff --git a/tests/sys/netpfil/common/pft_ping.py b/tests/sys/netpfil/common/pft_ping.py index a24a1e00150a..a2a1d9c7f4ec 100644 --- a/tests/sys/netpfil/common/pft_ping.py +++ b/tests/sys/netpfil/common/pft_ping.py @@ -33,6 +33,7 @@ logging.getLogger("scapy").setLevel(logging.CRITICAL) import math import scapy.all as sp import sys +import socket from copy import copy from sniffer import Sniffer @@ -49,8 +50,17 @@ def build_payload(l): return ret -def prepare_ipv6(dst_address, send_params): +def clean_params(params): + # Prepare a copy of safe copy of params + ret = copy(params) + ret.pop('src_address') + ret.pop('dst_address') + ret.pop('flags') + return ret + +def prepare_ipv6(send_params): src_address = send_params.get('src_address') + dst_address = send_params.get('dst_address') hlim = send_params.get('hlim') tc = send_params.get('tc') ip6 = sp.IPv6(dst=dst_address) @@ -63,8 +73,9 @@ def prepare_ipv6(dst_address, send_params): return ip6 -def prepare_ipv4(dst_address, send_params): +def prepare_ipv4(send_params): src_address = send_params.get('src_address') + dst_address = send_params.get('dst_address') flags = send_params.get('flags') tos = send_params.get('tc') ttl = send_params.get('hlim') @@ -84,22 +95,22 @@ def prepare_ipv4(dst_address, send_params): return ip -def send_icmp_ping(dst_address, sendif, send_params): +def send_icmp_ping(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) + if ':' in send_params['dst_address']: + ip6 = prepare_ipv6(send_params) icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length))) if send_frag_length: - for packet in sp.fragment(ip6 / icmp, fragsize=send_frag_length): + for packet in sp.fragment6(ip6 / icmp, fragSize=send_frag_length): packets.append(ether / packet) else: packets.append(ether / ip6 / icmp) else: - ip = prepare_ipv4(dst_address, send_params) + ip = prepare_ipv4(send_params) icmp = sp.ICMP(type='echo-request') raw = sp.raw(build_payload(send_length)) if send_frag_length: @@ -108,10 +119,10 @@ def send_icmp_ping(dst_address, sendif, send_params): else: packets.append(ether / ip / icmp / raw) for packet in packets: - sp.sendp(packet, sendif, verbose=False) + sp.sendp(packet, iface=send_params['sendif'], verbose=False) -def send_tcp_syn(dst_address, sendif, send_params): +def send_tcp_syn(send_params): tcpopt_unaligned = send_params.get('tcpopt_unaligned') seq = send_params.get('seq') mss = send_params.get('mss') @@ -119,22 +130,63 @@ def send_tcp_syn(dst_address, sendif, send_params): opts=[('Timestamp', (1, 1)), ('MSS', mss if mss else 1280)] if tcpopt_unaligned: opts = [('NOP', 0 )] + opts - if ':' in dst_address: - ip = prepare_ipv6(dst_address, send_params) + if ':' in send_params['dst_address']: + ip = prepare_ipv6(send_params) else: - ip = prepare_ipv4(dst_address, send_params) - tcp = sp.TCP(dport=666, flags='S', options=opts, seq=seq) + ip = prepare_ipv4(send_params) + tcp = sp.TCP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + flags='S', options=opts, seq=seq, + ) req = ether / ip / tcp - sp.sendp(req, iface=sendif, verbose=False) + sp.sendp(req, iface=send_params['sendif'], verbose=False) + + +def send_udp(send_params): + LOGGER.debug(f'Sending UDP ping') + packets = [] + send_length = send_params['length'] + send_frag_length = send_params['frag_length'] + ether = sp.Ether() + if ':' in send_params['dst_address']: + ip6 = prepare_ipv6(send_params) + udp = sp.UDP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + ) + raw = sp.Raw(load=build_payload(send_length)) + if send_frag_length: + for packet in sp.fragment6(ip6 / udp / raw, fragSize=send_frag_length): + packets.append(ether / packet) + else: + packets.append(ether / ip6 / udp / raw) + else: + ip = prepare_ipv4(send_params) + udp = sp.UDP( + sport=send_params.get('sport'), dport=send_params.get('dport'), + ) + raw = sp.Raw(load=build_payload(send_length)) + if send_frag_length: + for packet in sp.fragment(ip / udp / raw, fragsize=send_frag_length): + packets.append(ether / packet) + else: + packets.append(ether / ip / udp / raw) + + for packet in packets: + sp.sendp(packet, iface=send_params['sendif'], verbose=False) -def send_ping(dst_address, sendif, ping_type, send_params): +def send_ping(ping_type, send_params): if ping_type == 'icmp': - send_icmp_ping(dst_address, sendif, send_params) - elif ping_type == 'tcpsyn': - send_tcp_syn(dst_address, sendif, send_params) + send_icmp_ping(send_params) + elif ( + ping_type == 'tcpsyn' or + ping_type == 'tcp3way' + ): + send_tcp_syn(send_params) + elif ping_type == 'udp': + send_udp(send_params) else: - raise Exception('Unspported ping type') + raise Exception('Unsupported ping type') def check_ipv4(expect_params, packet): @@ -144,20 +196,15 @@ def check_ipv4(expect_params, packet): tos = expect_params.get('tc') ttl = expect_params.get('hlim') ip = packet.getlayer(sp.IP) + LOGGER.debug(f'Packet: {ip}') if not ip: LOGGER.debug('Packet is not IPv4!') return False if src_address and ip.src != src_address: - LOGGER.debug('Source IPv4 address does not match!') + LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}') return False if dst_address and ip.dst != dst_address: - LOGGER.debug('Destination IPv4 address does not match!') - return False - chksum = ip.chksum - ip.chksum = None - new_chksum = sp.IP(sp.raw(ip)).chksum - if chksum != new_chksum: - LOGGER.debug(f'Expected IP checksum {new_chksum} but found {chksum}') + LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}') return False if flags and ip.flags != flags: LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}') @@ -181,11 +228,13 @@ def check_ipv6(expect_params, packet): if not ip6: LOGGER.debug('Packet is not IPv6!') return False - if src_address and ip6.src != src_address: - LOGGER.debug('Source IPv6 address does not match!') + if src_address and socket.inet_pton(socket.AF_INET6, ip6.src) != \ + socket.inet_pton(socket.AF_INET6, src_address): + LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}') return False - if dst_address and ip6.dst != dst_address: - LOGGER.debug('Destination IPv6 address does not match!') + if dst_address and socket.inet_pton(socket.AF_INET6, ip6.dst) != \ + socket.inet_pton(socket.AF_INET6, dst_address): + LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}') return False # IPv6 has no IP-level checksum. if flags: @@ -265,32 +314,32 @@ def check_ping_reply_6(expect_params, packet): return True -def check_ping_request(expect_params, packet): - src_address = expect_params.get('src_address') - dst_address = expect_params.get('dst_address') +def check_ping_request(args, packet): + src_address = args['expect_params'].get('src_address') + dst_address = args['expect_params'].get('dst_address') if not (src_address or dst_address): raise Exception('Source or destination address must be given to match the ping request!') if ( (src_address and ':' in src_address) or (dst_address and ':' in dst_address) ): - return check_ping_request_6(expect_params, packet) + return check_ping_request_6(args['expect_params'], packet) else: - return check_ping_request_4(expect_params, packet) + return check_ping_request_4(args['expect_params'], packet) -def check_ping_reply(expect_params, packet): - src_address = expect_params.get('src_address') - dst_address = expect_params.get('dst_address') +def check_ping_reply(args, packet): + src_address = args['expect_params'].get('src_address') + dst_address = args['expect_params'].get('dst_address') if not (src_address or dst_address): raise Exception('Source or destination address must be given to match the ping reply!') if ( (src_address and ':' in src_address) or (dst_address and ':' in dst_address) ): - return check_ping_reply_6(expect_params, packet) + return check_ping_reply_6(args['expect_params'], packet) else: - return check_ping_reply_4(expect_params, packet) + return check_ping_reply_4(args['expect_params'], packet) def check_tcp(expect_params, packet): @@ -305,7 +354,7 @@ def check_tcp(expect_params, packet): tcp.chksum = None newpacket = sp.Ether(sp.raw(packet[sp.Ether])) new_chksum = newpacket[sp.TCP].chksum - if chksum != new_chksum: + if new_chksum and chksum != new_chksum: LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!') return False if tcp_flags and tcp.flags != tcp_flags: @@ -328,6 +377,30 @@ def check_tcp(expect_params, packet): return True +def check_udp(expect_params, packet): + expect_length = expect_params['length'] + udp = packet.getlayer(sp.UDP) + if not udp: + LOGGER.debug('Packet is not UDP!') + return False + raw = packet.getlayer(sp.Raw) + if not raw: + LOGGER.debug('Packet contains no payload!') + return False + if raw.load != build_payload(expect_length): + LOGGER.debug(f'Payload magic does not match len {len(raw.load)} vs {expect_length}!') + return False + orig_chksum = udp.chksum + udp.chksum = None + newpacket = sp.Ether(sp.raw(packet[sp.Ether])) + new_chksum = newpacket[sp.UDP].chksum + if new_chksum and orig_chksum != new_chksum: + LOGGER.debug(f'Wrong UDP checksum {orig_chksum}, expected {new_chksum}!') + return False + + return True + + def check_tcp_syn_request_4(expect_params, packet): if not check_ipv4(expect_params, packet): return False @@ -336,7 +409,7 @@ def check_tcp_syn_request_4(expect_params, packet): return True -def check_tcp_syn_reply_4(expect_params, packet): +def check_tcp_syn_reply_4(send_params, expect_params, packet): if not check_ipv4(expect_params, packet): return False if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet): @@ -344,6 +417,44 @@ def check_tcp_syn_reply_4(expect_params, packet): return True +def check_tcp_3way_4(args, packet): + send_params = args['send_params'] + + expect_params_sa = clean_params(args['expect_params']) + expect_params_sa['src_address'] = send_params['dst_address'] + expect_params_sa['dst_address'] = send_params['src_address'] + + # Sniff incoming SYN+ACK packet + if ( + check_ipv4(expect_params_sa, packet) and + check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet) + ): + ether = sp.Ether() + ip_sa = packet.getlayer(sp.IP) + tcp_sa = packet.getlayer(sp.TCP) + reply_params = clean_params(send_params) + reply_params['src_address'] = ip_sa.dst + reply_params['dst_address'] = ip_sa.src + ip_a = prepare_ipv4(reply_params) + tcp_a = sp.TCP( + sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A', + seq=tcp_sa.ack, ack=tcp_sa.seq + 1, + ) + req = ether / ip_a / tcp_a + sp.sendp(req, iface=send_params['sendif'], verbose=False) + return True + + return False + + +def check_udp_request_4(expect_params, packet): + if not check_ipv4(expect_params, packet): + return False + if not check_udp(expect_params, packet): + return False + return True + + def check_tcp_syn_request_6(expect_params, packet): if not check_ipv6(expect_params, packet): return False @@ -360,7 +471,45 @@ def check_tcp_syn_reply_6(expect_params, packet): return True -def check_tcp_syn_request(expect_params, packet): +def check_tcp_3way_6(args, packet): + send_params = args['send_params'] + + expect_params_sa = clean_params(args['expect_params']) + expect_params_sa['src_address'] = send_params['dst_address'] + expect_params_sa['dst_address'] = send_params['src_address'] + + # Sniff incoming SYN+ACK packet + if ( + check_ipv6(expect_params_sa, packet) and + check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet) + ): + ether = sp.Ether() + ip6_sa = packet.getlayer(sp.IPv6) + tcp_sa = packet.getlayer(sp.TCP) + reply_params = clean_params(send_params) + reply_params['src_address'] = ip6_sa.dst + reply_params['dst_address'] = ip6_sa.src + ip_a = prepare_ipv6(reply_params) + tcp_a = sp.TCP( + sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A', + seq=tcp_sa.ack, ack=tcp_sa.seq + 1, + ) + req = ether / ip_a / tcp_a + sp.sendp(req, iface=send_params['sendif'], verbose=False) + return True + + return False + + +def check_udp_request_6(expect_params, packet): + if not check_ipv6(expect_params, packet): + return False + if not check_udp(expect_params, packet): + return False + return True + +def check_tcp_syn_request(args, packet): + expect_params = args['expect_params'] src_address = expect_params.get('src_address') dst_address = expect_params.get('dst_address') if not (src_address or dst_address): @@ -374,7 +523,8 @@ def check_tcp_syn_request(expect_params, packet): return check_tcp_syn_request_4(expect_params, packet) -def check_tcp_syn_reply(expect_params, packet): +def check_tcp_syn_reply(args, packet): + expect_params = args['expect_params'] src_address = expect_params.get('src_address') dst_address = expect_params.get('dst_address') if not (src_address or dst_address): @@ -387,8 +537,39 @@ def check_tcp_syn_reply(expect_params, packet): else: return check_tcp_syn_reply_4(expect_params, packet) +def check_tcp_3way(args, packet): + expect_params = args['expect_params'] + src_address = expect_params.get('src_address') + dst_address = expect_params.get('dst_address') + if not (src_address or dst_address): + raise Exception('Source or destination address must be given to match the tcp syn reply!') + if ( + (src_address and ':' in src_address) or + (dst_address and ':' in dst_address) + ): + return check_tcp_3way_6(args, packet) + else: + return check_tcp_3way_4(args, packet) + -def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag): +def check_udp_request(args, packet): + expect_params = args['expect_params'] + src_address = expect_params.get('src_address') + dst_address = expect_params.get('dst_address') + if not (src_address or dst_address): + raise Exception('Source or destination address must be given to match the tcp syn request!') + if ( + (src_address and ':' in src_address) or + (dst_address and ':' in dst_address) + ): + return check_udp_request_6(expect_params, packet) + else: + return check_udp_request_4(expect_params, packet) + + +def setup_sniffer( + recvif, ping_type, sniff_type, expect_params, defrag, send_params, +): if ping_type == 'icmp' and sniff_type == 'request': checkfn = check_ping_request elif ping_type == 'icmp' and sniff_type == 'reply': @@ -397,10 +578,17 @@ def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag): checkfn = check_tcp_syn_request elif ping_type == 'tcpsyn' and sniff_type == 'reply': checkfn = check_tcp_syn_reply + elif ping_type == 'tcp3way' and sniff_type == 'reply': + checkfn = check_tcp_3way + elif ping_type == 'udp' and sniff_type == 'request': + checkfn = check_udp_request else: - raise Exception('Unspported ping or sniff type') + raise Exception('Unspported ping and sniff type combination') - return Sniffer(expect_params, checkfn, recvif, defrag=defrag) + return Sniffer( + {'send_params': send_params, 'expect_params': expect_params}, + checkfn, recvif, defrag=defrag, + ) def parse_args(): @@ -408,17 +596,15 @@ def parse_args(): description="Ping test tool") # Parameters of sent ping request - parser.add_argument('--sendif', nargs=1, - required=True, + parser.add_argument('--sendif', required=True, help='The interface through which the packet(s) will be sent') - parser.add_argument('--to', nargs=1, - required=True, + parser.add_argument('--to', required=True, help='The destination IP address for the ping request') parser.add_argument('--ping-type', - choices=('icmp', 'tcpsyn'), - help='Type of ping: ICMP (default) or TCP SYN', + choices=('icmp', 'tcpsyn', 'tcp3way', 'udp'), + help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake', default='icmp') - parser.add_argument('--fromaddr', nargs=1, + parser.add_argument('--fromaddr', help='The source IP address for the ping request') # Where to look for packets to analyze. @@ -431,36 +617,40 @@ def parse_args(): # Packet settings parser_send = parser.add_argument_group('Values set in transmitted packets') - parser_send.add_argument('--send-flags', nargs=1, type=str, + parser_send.add_argument('--send-flags', 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, + parser_send.add_argument('--send-frag-length', type=int, + help='Force IP fragmentation with given fragment length') + parser_send.add_argument('--send-hlim', type=int, help='IPv6 Hop Limit or IPv4 Time To Live') - parser_send.add_argument('--send-mss', nargs=1, type=int, + parser_send.add_argument('--send-mss', type=int, help='TCP Maximum Segment Size') - parser_send.add_argument('--send-seq', nargs=1, type=int, + parser_send.add_argument('--send-seq', type=int, help='TCP sequence number') - parser_send.add_argument('--send-length', nargs=1, type=int, - default=[len(PAYLOAD_MAGIC)], help='ICMP Echo Request payload size') - parser_send.add_argument('--send-tc', nargs=1, type=int, + parser_send.add_argument('--send-sport', type=int, + help='TCP source port') + parser_send.add_argument('--send-dport', type=int, default=9, + help='TCP destination port') + parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC), + help='ICMP Echo Request payload size') + parser_send.add_argument('--send-tc', 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') parser_send.add_argument('--send-nop', action='store_true', - help='Include a NOP IPv4 option') + help='Include a NOP IPv4 option') # Expectations parser_expect = parser.add_argument_group('Values expected in sniffed packets') - parser_expect.add_argument('--expect-flags', nargs=1, type=str, + parser_expect.add_argument('--expect-flags', type=str, help='IPv4 fragmentation flags') - parser_expect.add_argument('--expect-hlim', nargs=1, type=int, + parser_expect.add_argument('--expect-hlim', type=int, help='IPv6 Hop Limit or IPv4 Time To Live') - parser_expect.add_argument('--expect-mss', nargs=1, type=int, + parser_expect.add_argument('--expect-mss', type=int, help='TCP Maximum Segment Size') - parser_send.add_argument('--expect-seq', nargs=1, type=int, + parser_send.add_argument('--expect-seq', type=int, help='TCP sequence number') - parser_expect.add_argument('--expect-tc', nargs=1, type=int, + parser_expect.add_argument('--expect-tc', type=int, help='IPv6 Traffic Class or IPv4 DiffServ / ToS') parser.add_argument('-v', '--verbose', action='store_true', @@ -478,31 +668,31 @@ def main(): if args.verbose: LOGGER.setLevel(logging.DEBUG) - # Dig out real values of program arguments - send_if = args.sendif[0] - reply_ifs = args.replyif - recv_ifs = args.recvif - dst_address = args.to[0] - - # Standardize parameters which have nargs=1. + # Split parameters into send and expect parameters. Parameters might be + # missing from the command line, always fill the dictionaries with None. send_params = {} expect_params = {} - for param_name in ('flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length'): + for param_name in ( + 'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length', + 'sport', 'dport', + ): param_arg = vars(args).get(f'send_{param_name}') - send_params[param_name] = param_arg[0] if param_arg else None + send_params[param_name] = param_arg if param_arg else None param_arg = vars(args).get(f'expect_{param_name}') - expect_params[param_name] = param_arg[0] if param_arg else None + expect_params[param_name] = param_arg if param_arg else None expect_params['length'] = send_params['length'] send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned send_params['nop'] = args.send_nop - send_params['src_address'] = args.fromaddr[0] if args.fromaddr else None + send_params['src_address'] = args.fromaddr if args.fromaddr else None + send_params['dst_address'] = args.to + send_params['sendif'] = args.sendif # We may not have a default route. Tell scapy where to start looking for routes - sp.conf.iface6 = send_if + sp.conf.iface6 = args.sendif # Configuration sanity checking. - if not (reply_ifs or recv_ifs): + if not (args.replyif or args.recvif): raise Exception('With no reply or recv interface specified no traffic ' 'can be sniffed and verified!' ) @@ -510,35 +700,41 @@ def main(): sniffers = [] if send_params['frag_length']: - defrag = True + if ( + (send_params['src_address'] and ':' in send_params['src_address']) or + (send_params['dst_address'] and ':' in send_params['dst_address']) + ): + defrag = 'IPv6' + else: + defrag = 'IPv4' else: defrag = False - if recv_ifs: + if args.recvif: sniffer_params = copy(expect_params) sniffer_params['src_address'] = None - sniffer_params['dst_address'] = dst_address - for iface in recv_ifs: + sniffer_params['dst_address'] = args.to + for iface in args.recvif: LOGGER.debug(f'Installing receive sniffer on {iface}') sniffers.append( setup_sniffer(iface, args.ping_type, 'request', - sniffer_params, defrag, + sniffer_params, defrag, send_params, )) - if reply_ifs: + if args.replyif: sniffer_params = copy(expect_params) - sniffer_params['src_address'] = dst_address + sniffer_params['src_address'] = args.to sniffer_params['dst_address'] = None - for iface in reply_ifs: + for iface in args.replyif: LOGGER.debug(f'Installing reply sniffer on {iface}') sniffers.append( setup_sniffer(iface, args.ping_type, 'reply', - sniffer_params, defrag, + sniffer_params, defrag, send_params, )) LOGGER.debug(f'Installed {len(sniffers)} sniffers') - send_ping(dst_address, send_if, args.ping_type, send_params) + send_ping(args.ping_type, send_params) err = 0 sniffer_num = 0 diff --git a/tests/sys/netpfil/common/rdr.sh b/tests/sys/netpfil/common/rdr.sh index 7d6297870e6c..0d6f27694c8c 100644 --- a/tests/sys/netpfil/common/rdr.sh +++ b/tests/sys/netpfil/common/rdr.sh @@ -83,6 +83,7 @@ local_redirect_body() firewall=$1 firewall_init $firewall nat_init $firewall + vnet_init_bridge bridge=$(vnet_mkbridge) ifconfig ${bridge} 192.0.2.1/24 up diff --git a/tests/sys/netpfil/common/sniffer.py b/tests/sys/netpfil/common/sniffer.py index 14305a37278c..583b27d34ca6 100644 --- a/tests/sys/netpfil/common/sniffer.py +++ b/tests/sys/netpfil/common/sniffer.py @@ -56,14 +56,19 @@ class Sniffer(threading.Thread): def run(self): self.packets = [] - 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. + # 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. + if self._defrag == 'IPv4': 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) + elif self._defrag == 'IPv6': + self.packets = sp.sniff(session=sp.DefaultSession, iface=self._recvif, + timeout=self._timeout, started_callback=self._startedCb) + for p in sp.defragment6(self.packets): + self._checkPacket(p) else: self.packets = sp.sniff(iface=self._recvif, stop_filter=self._checkPacket, timeout=self._timeout, diff --git a/tests/sys/netpfil/common/tos.sh b/tests/sys/netpfil/common/tos.sh index 39f756be8fe9..3b689d7f67d0 100644 --- a/tests/sys/netpfil/common/tos.sh +++ b/tests/sys/netpfil/common/tos.sh @@ -33,7 +33,7 @@ tos_head() { atf_set descr 'set-tos test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } tos_body() diff --git a/tests/sys/netpfil/ipfw/Makefile b/tests/sys/netpfil/ipfw/Makefile index 1d4629c0e738..d4dbdb00f251 100644 --- a/tests/sys/netpfil/ipfw/Makefile +++ b/tests/sys/netpfil/ipfw/Makefile @@ -2,7 +2,13 @@ PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/ipfw -ATF_TESTS_SH+= fwd +ATF_TESTS_SH+= fwd \ + divert + ${PACKAGE}FILES+= fwd_inetd.conf +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" + .include <bsd.test.mk> diff --git a/tests/sys/netpfil/ipfw/divert.sh b/tests/sys/netpfil/ipfw/divert.sh new file mode 100644 index 000000000000..62db3f8fce98 --- /dev/null +++ b/tests/sys/netpfil/ipfw/divert.sh @@ -0,0 +1,281 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +# +# ipfw divert action test cases +# +# -----------| |-- |----| ----| |----------- +# ( ) inbound |ipfw| ) -> |host| -> ( ) |ipfw| outbound ) +# -----------| | |-- |----| ----| | |----------- +# | | +# \|/ \|/ +# |------| |------| +# |divapp| |divapp| +# |------| |------| +# +# The basic cases: +# - inbound > diverted | divapp terminated +# - inbound > diverted > inbound | host terminated +# - inbound > diverted > outbound | network terminated +# - outbound > diverted | divapp terminated +# - outbound > diverted > outbound | network terminated +# - outbound > diverted > inbound | e.g. host terminated +# +# When a packet is diverted, forwarded, and possibly diverted again: +# - inbound > diverted > inbound > forwarded +# > outbound | network terminated +# - inbound > diverted > inbound > forwarded +# > outbound > diverted > outbound | network terminated +# +# Test case naming legend: +# in - inbound +# div - diverted +# out - outbound +# fwd - forwarded +# + +. $(atf_get_srcdir)/../common/utils.subr + +divert_init() +{ + if ! kldstat -q -m ipdivert; then + atf_skip "This test requires ipdivert" + fi +} + +atf_test_case "in_div" "cleanup" +in_div_head() +{ + atf_set descr 'Test inbound > diverted | divapp terminated' + atf_set require.user root +} +in_div_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is expected to "eat" the packet + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + wait $divapp_pid +} +in_div_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "in_div_in" "cleanup" +in_div_in_head() +{ + atf_set descr 'Test inbound > diverted > inbound | host terminated' + atf_set require.user root +} +in_div_in_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + wait $divapp_pid +} +in_div_in_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "out_div" "cleanup" +out_div_head() +{ + atf_set descr 'Test outbound > diverted | divapp terminated' + atf_set require.user root +} +out_div_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is expected to "eat" the packet + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + wait $divapp_pid +} +out_div_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "out_div_out" "cleanup" +out_div_out_head() +{ + atf_set descr 'Test outbound > diverted > outbound | network terminated' + atf_set require.user root +} +out_div_out_body() +{ + firewall_init "ipfw" + divert_init + + epair=$(vnet_mkepair) + vnet_mkjail div ${epair}b + ifconfig ${epair}a 192.0.2.1/24 up + jexec div ifconfig ${epair}b 192.0.2.2/24 up + jexec div ipfw add 65534 allow all from any to any + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0 + + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & + divapp_pid=$! + # Wait for the divapp to be ready + sleep 1 + + # divapp is NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + wait $divapp_pid +} +out_div_out_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_test_case "in_div_in_fwd_out_div_out" "cleanup" +in_div_in_fwd_out_div_out_head() +{ + atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated' + atf_set require.user root +} +in_div_in_fwd_out_div_out_body() +{ + firewall_init "ipfw" + divert_init + + # host <a--epair0--b> router <a--epair1--b> site + epair0=$(vnet_mkepair) + epair1=$(vnet_mkepair) + + vnet_mkjail router ${epair0}b ${epair1}a + ifconfig ${epair0}a 192.0.2.1/24 up + jexec router sysctl net.inet.ip.forwarding=1 + jexec router ifconfig ${epair0}b 192.0.2.2/24 up + jexec router ifconfig ${epair1}a 198.51.100.1/24 up + jexec router ipfw add 65534 allow all from any to any + + vnet_mkjail site ${epair1}b + jexec site ifconfig ${epair1}b 198.51.100.2/24 up + jexec site ipfw add 65534 allow all from any to any + jexec site route add default 198.51.100.1 + + route add -net 198.51.100.0/24 192.0.2.2 + + # Sanity check + atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 + + # Should be routed without diversion + atf_check -s exit:0 -o ignore ping -c3 198.51.100.2 + + jexec router ipfw add 100 divert 2001 icmp from any to any in icmptypes 8 + jexec router ipfw add 200 divert 2002 icmp from any to any out icmptypes 8 + + jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back & + indivapp_pid=$! + jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back & + outdivapp_pid=$! + # Wait for the divappS to be ready + sleep 1 + + # Both divappS are NOT expected to "eat" the packet + atf_check -s exit:0 -o ignore ping -c1 198.51.100.2 + + wait $indivapp_pid && wait $outdivapp_pid +} +in_div_in_fwd_out_div_out_cleanup() +{ + firewall_cleanup "ipfw" +} + +atf_init_test_cases() +{ + atf_add_test_case "in_div" + atf_add_test_case "in_div_in" + + atf_add_test_case "out_div" + atf_add_test_case "out_div_out" + + atf_add_test_case "in_div_in_fwd_out_div_out" +} diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile index 867b98e5f6c2..3adaef09ddbd 100644 --- a/tests/sys/netpfil/pf/Makefile +++ b/tests/sys/netpfil/pf/Makefile @@ -1,12 +1,11 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/pf -BINDIR= ${TESTSDIR} TESTS_SUBDIRS+= ioctl ATF_TESTS_SH+= altq \ anchor \ + debug \ divert-to \ dup \ ether \ @@ -16,14 +15,21 @@ ATF_TESTS_SH+= altq \ fragmentation_no_reassembly \ get_state \ icmp \ + icmp6 \ + if_enc \ + limits \ loginterface \ killstate \ macro \ - map_e \ match \ + max_pkt_rate \ + max_pkt_size \ + max_states \ + mbuf \ modulate \ names \ nat \ + nat64 \ pass_block \ pflog \ pflow \ @@ -40,7 +46,9 @@ ATF_TESTS_SH+= altq \ sctp \ set_skip \ set_tos \ + snmp \ src_track \ + status \ syncookie \ synproxy \ table \ @@ -48,15 +56,21 @@ ATF_TESTS_SH+= altq \ tos ATF_TESTS_PYTEST+= frag6.py +ATF_TESTS_PYTEST+= header.py +ATF_TESTS_PYTEST+= icmp.py +ATF_TESTS_PYTEST+= nat64.py ATF_TESTS_PYTEST+= nat66.py +ATF_TESTS_PYTEST+= return.py ATF_TESTS_PYTEST+= sctp.py +ATF_TESTS_PYTEST+= tcp.py -# Tests reuse jail names and so cannot run in parallel. -TEST_METADATA+= is_exclusive=true - -PROGS= divapp +# Allow tests to run in parallel in their own jails +TEST_METADATA+= execenv="jail" +TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets" -${PACKAGE}FILES+= CVE-2019-5597.py \ +${PACKAGE}FILES+= \ + bsnmpd.conf \ + CVE-2019-5597.py \ CVE-2019-5598.py \ daytime_inetd.conf \ echo_inetd.conf \ @@ -64,17 +78,24 @@ ${PACKAGE}FILES+= CVE-2019-5597.py \ frag-overindex.py \ frag-overlimit.py \ frag-overreplace.py \ + frag-overhole.py \ + frag-adjhole.py \ pfsync_defer.py \ pft_ether.py \ pft_read_ipfix.py \ - utils.subr + rdr-srcport.py \ + utils.subr \ + utils.py +${PACKAGE}FILESMODE_bsnmpd.conf= 0555 ${PACKAGE}FILESMODE_CVE-2019-5597.py= 0555 ${PACKAGE}FILESMODE_CVE-2019-5598.py= 0555 ${PACKAGE}FILESMODE_fragcommon.py= 0555 ${PACKAGE}FILESMODE_frag-overindex.py= 0555 ${PACKAGE}FILESMODE_frag-overlimit.py= 0555 ${PACKAGE}FILESMODE_frag-overreplace.py= 0555 +${PACKAGE}FILESMODE_frag-overhole.py= 0555 +${PACKAGE}FILESMODE_frag-adjhole.py= 0555 ${PACKAGE}FILESMODE_pfsync_defer.py= 0555 ${PACKAGE}FILESMODE_pft_ether.py= 0555 ${PACKAGE}FILESMODE_pft_read_ipfix.py= 0555 diff --git a/tests/sys/netpfil/pf/altq.sh b/tests/sys/netpfil/pf/altq.sh index 57a9edf11366..416a55777849 100644 --- a/tests/sys/netpfil/pf/altq.sh +++ b/tests/sys/netpfil/pf/altq.sh @@ -156,9 +156,7 @@ codel_bridge_body() { altq_init is_altq_supported codel - if ! kldstat -q -m if_bridge; then - atf_skip "This test requires if_bridge" - fi + vnet_init_bridge epair=$(vnet_mkepair) ifconfig ${epair}a 192.0.2.1/24 up @@ -214,7 +212,7 @@ prioritise_body() ifconfig ${epair}a 192.0.2.1/24 up jexec altq_prioritise ifconfig ${epair}b 192.0.2.2/24 up - jexec altq_prioritise /usr/sbin/inetd -p inetd-altq.pid \ + jexec altq_prioritise /usr/sbin/inetd -p ${PWD}/inetd-altq.pid \ $(atf_get_srcdir)/../pf/echo_inetd.conf # Sanity check diff --git a/tests/sys/netpfil/pf/anchor.sh b/tests/sys/netpfil/pf/anchor.sh index b1faa5f6c57e..b4b52d7a24d6 100644 --- a/tests/sys/netpfil/pf/anchor.sh +++ b/tests/sys/netpfil/pf/anchor.sh @@ -58,6 +58,37 @@ pr183198_cleanup() pft_cleanup } +atf_test_case "pr279225" "cleanup" +pr279225_head() +{ + atf_set descr "Test that we can retrieve longer anchor names, PR 279225" + atf_set require.user root +} + +pr279225_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "nat-anchor \"appjail-nat/jail/*\" all" \ + "rdr-anchor \"appjail-rdr/*\" all" \ + "anchor \"appjail/jail/*\" all" + + atf_check -s exit:0 -o match:"nat-anchor \"appjail-nat/jail/\*\" all \{" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"rdr-anchor \"appjail-rdr/\*\" all \{" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"anchor \"appjail/jail/\*\" all \{" \ + jexec alcatraz pfctl -sr -a "*" +} + +pr279225_cleanup() +{ + pft_cleanup +} + atf_test_case "nested_anchor" "cleanup" nested_anchor_head() { @@ -164,10 +195,259 @@ nested_label_cleanup() pft_cleanup } +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr "Test handling of quick on anchors" + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + }" \ + "block" + + # We can still ping because the anchor is 'quick' + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v +} + +quick_cleanup() +{ + pft_cleanup +} + +atf_test_case "quick_nested" "cleanup" +quick_nested_head() +{ + atf_set descr 'Verify that a nested anchor does not clear quick' + atf_set require.user root +} + +quick_nested_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + anchor {\n\ + block proto tcp\n\ + }\n\ + }" \ + "block" + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v + + # We can still ping because the anchor is 'quick' + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -v + jexec alcatraz pfctl -ss -v +} + +quick_nested_cleanup() +{ + pft_cleanup +} + +atf_test_case "counter" "cleanup" +counter_head() +{ + atf_set descr 'Test counters on anchors' + atf_set require.user root +} + +counter_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor \"foo\" {\n\ + pass\n\ + }" + + # Generate traffic + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + atf_check -s exit:0 -e ignore \ + -o match:'[ Evaluations: 1 Packets: 2 Bytes: 168 States: 1 ]' \ + jexec alcatraz pfctl -sr -vv +} + +counter_cleanup() +{ + pft_cleanup +} + +atf_test_case "nat" "cleanup" +nat_head() +{ + atf_set descr 'Test nested nat anchors' + atf_set require.user root +} + +nat_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "nat-anchor \"foo/*\"" \ + "pass" + + echo "nat log on ${epair}a inet from 192.0.2.0/24 to any port = 53 -> 192.0.2.1" \ + | jexec alcatraz pfctl -a "foo/bar" -g -f - + echo "rdr on ${epair}a proto tcp to port echo -> 127.0.0.1 port echo" \ + | jexec alcatraz pfctl -a "foo/baz" -g -f - + + jexec alcatraz pfctl -sn -a "*" + jexec alcatraz pfctl -sn -a "foo/bar" + jexec alcatraz pfctl -sn -a "foo/baz" + + atf_check -s exit:0 -o match:"nat log on epair0a inet from 192.0.2.0/24 to any port = domain -> 192.0.2.1" \ + jexec alcatraz pfctl -sn -a "*" + atf_check -s exit:0 -o match:"rdr on epair0a inet proto tcp from any to any port = echo -> 127.0.0.1 port 7" \ + jexec alcatraz pfctl -sn -a "*" +} + +nat_cleanup() +{ + pft_cleanup +} + +atf_test_case "include" "cleanup" +include_head() +{ + atf_set descr 'Test including inside anchors' + atf_set require.user root +} + +include_body() +{ + pft_init + + wd=`pwd` + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + echo "pass" > ${wd}/extra.conf + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\" {\n\ + include \"${wd}/extra.conf\"\n\ + }" + + jexec alcatraz pfctl -sr + + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 +} + +include_cleanup() +{ + pft_cleanup +} + +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr 'Test quick on anchors' + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "anchor quick {\n\ + pass\n\ + }" \ + "block" + + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + jexec alcatraz pfctl -sr -vv -a "*" +} + +quick_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "pr183198" + atf_add_test_case "pr279225" atf_add_test_case "nested_anchor" atf_add_test_case "wildcard" atf_add_test_case "nested_label" + atf_add_test_case "quick" + atf_add_test_case "quick_nested" + atf_add_test_case "counter" + atf_add_test_case "nat" + atf_add_test_case "include" + atf_add_test_case "quick" } diff --git a/tests/sys/netpfil/pf/bsnmpd.conf b/tests/sys/netpfil/pf/bsnmpd.conf new file mode 100644 index 000000000000..27abdda6cbd3 --- /dev/null +++ b/tests/sys/netpfil/pf/bsnmpd.conf @@ -0,0 +1,47 @@ +location := "A galaxy far, far away" +contact := "skywalker@Tatooine" +system := 1 + +read := "public" +write := "geheim" +trap := "mytrap" + +NoAuthProtocol := 1.3.6.1.6.3.10.1.1.1 +HMACMD5AuthProtocol := 1.3.6.1.6.3.10.1.1.2 +HMACSHAAuthProtocol := 1.3.6.1.6.3.10.1.1.3 +NoPrivProtocol := 1.3.6.1.6.3.10.1.2.1 +DESPrivProtocol := 1.3.6.1.6.3.10.1.2.2 +AesCfb128Protocol := 1.3.6.1.6.3.10.1.2.4 + +securityModelAny := 0 +securityModelSNMPv1 := 1 +securityModelSNMPv2c := 2 +securityModelUSM := 3 + +MPmodelSNMPv1 := 0 +MPmodelSNMPv2c := 1 +MPmodelSNMPv3 := 3 + +noAuthNoPriv := 1 +authNoPriv := 2 +authPriv := 3 + +%snmpd +begemotSnmpdDebugDumpPdus = 2 +begemotSnmpdDebugSyslogPri = 7 + +begemotSnmpdCommunityString.0.1 = $(read) +begemotSnmpdCommunityDisable = 1 + +begemotSnmpdTransInetStatus.1.4.0.0.0.0.161.1 = 4 +begemotSnmpdTransInetStatus.2.16.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.161.1 = 4 + +begemotSnmpdLocalPortStatus."/var/run/snmpd.sock" = 1 +begemotSnmpdLocalPortType."/var/run/snmpd.sock" = 4 + +sysContact = $(contact) +sysLocation = $(location) +sysObjectId = 1.3.6.1.4.1.12325.1.1.2.1.$(system) + +begemotSnmpdModulePath."mibII" = "/usr/lib/snmp_mibII.so" +begemotSnmpdModulePath."pf" = "/usr/lib/snmp_pf.so" diff --git a/tests/sys/netpfil/pf/debug.sh b/tests/sys/netpfil/pf/debug.sh new file mode 100644 index 000000000000..18a7febfbb5b --- /dev/null +++ b/tests/sys/netpfil/pf/debug.sh @@ -0,0 +1,56 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Test setting and retrieving debug level' + atf_set require.user root +} + +basic_body() +{ + pft_init + + vnet_mkjail debug + atf_check -s exit:0 -e ignore \ + jexec debug pfctl -x loud + + atf_check -s exit:0 -o match:'Debug: Loud' \ + jexec debug pfctl -si +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" +} diff --git a/tests/sys/netpfil/pf/divert-to.sh b/tests/sys/netpfil/pf/divert-to.sh index 72adbeedb007..ae44cd5d51af 100644 --- a/tests/sys/netpfil/pf/divert-to.sh +++ b/tests/sys/netpfil/pf/divert-to.sh @@ -51,8 +51,6 @@ # > outbound > diverted > outbound | network terminated # # Test case naming legend: -# ipfwon - with ipfw enabled -# ipfwoff - with ipfw disabled # in - inbound # div - diverted # out - outbound @@ -76,40 +74,21 @@ dummynet_init() fi } -ipfw_init() -{ - if ! kldstat -q -m ipfw; then - atf_skip "This test requires ipfw" - fi -} - -assert_ipfw_is_off() -{ - if kldstat -q -m ipfw; then - atf_skip "This test is for the case when ipfw is not loaded" - fi -} - -atf_test_case "ipfwoff_in_div" "cleanup" -ipfwoff_in_div_head() +atf_test_case "in_div" "cleanup" +in_div_head() { atf_set descr 'Test inbound > diverted | divapp terminated' atf_set require.user root } -ipfwoff_in_div_body() +in_div_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -119,56 +98,36 @@ ipfwoff_in_div_body() "pass all" \ "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000" - jexec div $(atf_get_srcdir)/divapp 2000 & + jexec div $(atf_get_srcdir)/../common/divapp 2000 & divapp_pid=$! # Wait for the divapp to be ready sleep 1 # divapp is expected to "eat" the packet - atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2 + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 wait $divapp_pid } -ipfwoff_in_div_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_div" "cleanup" -ipfwon_in_div_head() -{ - atf_set descr 'Test inbound > diverted | divapp terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_body() -{ - ipfwoff_in_div_body "ipfwon" -} -ipfwon_in_div_cleanup() +in_div_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_in_div_in" "cleanup" -ipfwoff_in_div_in_head() +atf_test_case "in_div_in" "cleanup" +in_div_in_head() { atf_set descr 'Test inbound > diverted > inbound | host terminated' atf_set require.user root } -ipfwoff_in_div_in_body() +in_div_in_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -178,7 +137,7 @@ ipfwoff_in_div_in_body() "pass all" \ "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 divert-back & + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & divapp_pid=$! # Wait for the divapp to be ready sleep 1 @@ -188,46 +147,26 @@ ipfwoff_in_div_in_body() wait $divapp_pid } -ipfwoff_in_div_in_cleanup() +in_div_in_cleanup() { pft_cleanup } -atf_test_case "ipfwon_in_div_in" "cleanup" -ipfwon_in_div_in_head() -{ - atf_set descr 'Test inbound > diverted > inbound | host terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_in_body() -{ - ipfwoff_in_div_in_body "ipfwon" -} -ipfwon_in_div_in_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwoff_out_div" "cleanup" -ipfwoff_out_div_head() +atf_test_case "out_div" "cleanup" +out_div_head() { atf_set descr 'Test outbound > diverted | divapp terminated' atf_set require.user root } -ipfwoff_out_div_body() +out_div_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -238,56 +177,36 @@ ipfwoff_out_div_body() "pass in inet proto icmp icmp-type echoreq no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 & + jexec div $(atf_get_srcdir)/../common/divapp 2000 & divapp_pid=$! # Wait for the divapp to be ready sleep 1 # divapp is expected to "eat" the packet - atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2 + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 wait $divapp_pid } -ipfwoff_out_div_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_out_div" "cleanup" -ipfwon_out_div_head() -{ - atf_set descr 'Test outbound > diverted | divapp terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_out_div_body() -{ - ipfwoff_out_div_body "ipfwon" -} -ipfwon_out_div_cleanup() +out_div_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_out_div_out" "cleanup" -ipfwoff_out_div_out_head() +atf_test_case "out_div_out" "cleanup" +out_div_out_head() { atf_set descr 'Test outbound > diverted > outbound | network terminated' atf_set require.user root } -ipfwoff_out_div_out_body() +out_div_out_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail div ${epair}b ifconfig ${epair}a 192.0.2.1/24 up jexec div ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec div ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -298,7 +217,7 @@ ipfwoff_out_div_out_body() "pass in inet proto icmp icmp-type echoreq no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state" - jexec div $(atf_get_srcdir)/divapp 2000 divert-back & + jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back & divapp_pid=$! # Wait for the divapp to be ready sleep 1 @@ -308,40 +227,21 @@ ipfwoff_out_div_out_body() wait $divapp_pid } -ipfwoff_out_div_out_cleanup() +out_div_out_cleanup() { pft_cleanup } -atf_test_case "ipfwon_out_div_out" "cleanup" -ipfwon_out_div_out_head() -{ - atf_set descr 'Test outbound > diverted > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_out_div_out_body() -{ - ipfwoff_out_div_out_body "ipfwon" -} -ipfwon_out_div_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwoff_in_div_in_fwd_out_div_out" "cleanup" -ipfwoff_in_div_in_fwd_out_div_out_head() +atf_test_case "in_div_in_fwd_out_div_out" "cleanup" +in_div_in_fwd_out_div_out_head() { atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated' atf_set require.user root } -ipfwoff_in_div_in_fwd_out_div_out_body() +in_div_in_fwd_out_div_out_body() { - local ipfwon - pft_init divert_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off # host <a--epair0--b> router <a--epair1--b> site epair0=$(vnet_mkepair) @@ -352,12 +252,10 @@ ipfwoff_in_div_in_fwd_out_div_out_body() jexec router sysctl net.inet.ip.forwarding=1 jexec router ifconfig ${epair0}b 192.0.2.2/24 up jexec router ifconfig ${epair1}a 198.51.100.1/24 up - test $ipfwon && jexec router ipfw add 65534 allow all from any to any vnet_mkjail site ${epair1}b jexec site ifconfig ${epair1}b 198.51.100.2/24 up jexec site route add default 198.51.100.1 - test $ipfwon && jexec site ipfw add 65534 allow all from any to any route add -net 198.51.100.0/24 192.0.2.2 @@ -373,9 +271,9 @@ ipfwoff_in_div_in_fwd_out_div_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \ "pass out inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2002 no state" - jexec router $(atf_get_srcdir)/divapp 2001 divert-back & + jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back & indivapp_pid=$! - jexec router $(atf_get_srcdir)/divapp 2002 divert-back & + jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -385,48 +283,28 @@ ipfwoff_in_div_in_fwd_out_div_out_body() wait $indivapp_pid && wait $outdivapp_pid } -ipfwoff_in_div_in_fwd_out_div_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_div_in_fwd_out_div_out" "cleanup" -ipfwon_in_div_in_fwd_out_div_out_head() -{ - atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_div_in_fwd_out_div_out_body() -{ - ipfwoff_in_div_in_fwd_out_div_out_body "ipfwon" -} -ipfwon_in_div_in_fwd_out_div_out_cleanup() +in_div_in_fwd_out_div_out_cleanup() { pft_cleanup } -atf_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out" "cleanup" -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_head() +atf_test_case "in_dn_in_div_in_out_div_out_dn_out" "cleanup" +in_dn_in_div_in_out_div_out_dn_out_head() { atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated' atf_set require.user root } -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() +in_dn_in_div_in_out_div_out_dn_out_body() { - local ipfwon - pft_init divert_init dummynet_init - test "$1" == "ipfwon" && ipfwon="yes" - test $ipfwon && ipfw_init || assert_ipfw_is_off epair=$(vnet_mkepair) vnet_mkjail alcatraz ${epair}b ifconfig ${epair}a 192.0.2.1/24 up ifconfig ${epair}a ether 02:00:00:00:00:01 jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - test $ipfwon && jexec alcatraz ipfw add 65534 allow all from any to any # Sanity check atf_check -s exit:0 -o ignore ping -c3 192.0.2.2 @@ -444,9 +322,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 1001 no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 1002 no state" - jexec alcatraz $(atf_get_srcdir)/divapp 1001 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 1001 divert-back & indivapp_pid=$! - jexec alcatraz $(atf_get_srcdir)/divapp 1002 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 1002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -473,9 +351,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() "pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \ "pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2002 no state" - jexec alcatraz $(atf_get_srcdir)/divapp 2001 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 2001 divert-back & indivapp_pid=$! - jexec alcatraz $(atf_get_srcdir)/divapp 2002 divert-back & + jexec alcatraz $(atf_get_srcdir)/../common/divapp 2002 divert-back & outdivapp_pid=$! # Wait for the divappS to be ready sleep 1 @@ -489,41 +367,20 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body() # } } -ipfwoff_in_dn_in_div_in_out_div_out_dn_out_cleanup() -{ - pft_cleanup -} - -atf_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out" "cleanup" -ipfwon_in_dn_in_div_in_out_div_out_dn_out_head() -{ - atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated, with ipfw enabled' - atf_set require.user root -} -ipfwon_in_dn_in_div_in_out_div_out_dn_out_body() -{ - ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body "ipfwon" -} -ipfwon_in_dn_in_div_in_out_div_out_dn_out_cleanup() +in_dn_in_div_in_out_div_out_dn_out_cleanup() { pft_cleanup } atf_init_test_cases() { - atf_add_test_case "ipfwoff_in_div" - atf_add_test_case "ipfwoff_in_div_in" - atf_add_test_case "ipfwon_in_div" - atf_add_test_case "ipfwon_in_div_in" + atf_add_test_case "in_div" + atf_add_test_case "in_div_in" - atf_add_test_case "ipfwoff_out_div" - atf_add_test_case "ipfwoff_out_div_out" - atf_add_test_case "ipfwon_out_div" - atf_add_test_case "ipfwon_out_div_out" + atf_add_test_case "out_div" + atf_add_test_case "out_div_out" - atf_add_test_case "ipfwoff_in_div_in_fwd_out_div_out" - atf_add_test_case "ipfwon_in_div_in_fwd_out_div_out" + atf_add_test_case "in_div_in_fwd_out_div_out" - atf_add_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out" - atf_add_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out" + atf_add_test_case "in_dn_in_div_in_out_div_out_dn_out" } diff --git a/tests/sys/netpfil/pf/dup.sh b/tests/sys/netpfil/pf/dup.sh index 68631251144a..64a08083bca0 100644 --- a/tests/sys/netpfil/pf/dup.sh +++ b/tests/sys/netpfil/pf/dup.sh @@ -33,7 +33,7 @@ dup_to_head() { atf_set descr 'dup-to test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } dup_to_body() diff --git a/tests/sys/netpfil/pf/ether.sh b/tests/sys/netpfil/pf/ether.sh index 9a1ab1b005d7..f0fdce50a7d3 100644 --- a/tests/sys/netpfil/pf/ether.sh +++ b/tests/sys/netpfil/pf/ether.sh @@ -286,7 +286,7 @@ captive_body() # Run the echo server only on the gw, so we know we've redirectly # correctly if we get an echo message. - jexec gw /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf + jexec gw /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf # Confirm that we're getting redirected atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7" @@ -304,7 +304,7 @@ captive_body() atf_check -s exit:1 -x "echo foo | nc -N 198.51.100.2 7" # Start a server in srv - jexec srv /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf # And now we can talk to that one. atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7" @@ -362,8 +362,8 @@ captive_long_body() # ICMP should still work, because we don't redirect it. atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2 - jexec gw /usr/sbin/inetd -p gw.pid $(atf_get_srcdir)/echo_inetd.conf - jexec srv /usr/sbin/inetd -p srv.pid $(atf_get_srcdir)/daytime_inetd.conf + jexec gw /usr/sbin/inetd -p ${PWD}/gw.pid $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/srv.pid $(atf_get_srcdir)/daytime_inetd.conf echo foo | nc -N 198.51.100.2 13 @@ -415,7 +415,7 @@ dummynet_body() # Sanity check atf_check -s exit:0 -o ignore ping -i .1 -c 3 -s 1200 192.0.2.2 - jexec alcatraz dnctl pipe 1 config bw 30Byte/s + jexec alcatraz dnctl pipe 1 config bw 300Byte/s jexec alcatraz pfctl -e pft_set_rules alcatraz \ "ether pass in dnpipe 1" @@ -430,14 +430,14 @@ dummynet_body() ping -i .1 -c 5 -s 1200 192.0.2.2 # We should now be hitting the limits and get this packet dropped. - atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2 + atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2 # We can now also dummynet outbound traffic! pft_set_rules alcatraz \ "ether pass out dnpipe 1" # We should still be hitting the limits and get this packet dropped. - atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2 + atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2 } dummynet_cleanup() @@ -643,7 +643,7 @@ short_pkt_head() { atf_set descr 'Test overly short Ethernet packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } short_pkt_body() @@ -686,7 +686,7 @@ bridge_to_head() { atf_set descr 'Test bridge-to keyword' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } bridge_to_body() diff --git a/tests/sys/netpfil/pf/forward.sh b/tests/sys/netpfil/pf/forward.sh index 31abfad82c13..5d7d48a5dd9a 100644 --- a/tests/sys/netpfil/pf/forward.sh +++ b/tests/sys/netpfil/pf/forward.sh @@ -35,7 +35,7 @@ v4_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -94,7 +94,7 @@ v6_head() { atf_set descr 'Basic IPv6 forwarding test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() diff --git a/tests/sys/netpfil/pf/frag-adjhole.py b/tests/sys/netpfil/pf/frag-adjhole.py new file mode 100644 index 000000000000..99caf66617dd --- /dev/null +++ b/tests/sys/netpfil/pf/frag-adjhole.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org> + +from fragcommon import * + +# |--------| +# |--------| +# |-------| +# |----| + +def send(src, dst, send_if, recv_if): + pid = os.getpid() + eid = pid & 0xffff + payload = b"ABCDEFGHIJKLMNOP" * 2 + packet = sp.IP(src=src, dst=dst)/ \ + sp.ICMP(type='echo-request', id=eid) / payload + frag = [] + fid = pid & 0xffff + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + flags='MF') / bytes(packet)[20:36]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=2, flags='MF') / bytes(packet)[36:52]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=1, flags='MF') / bytes(packet)[28:44]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=4) / bytes(packet)[52:60]) + eth=[] + for f in frag: + eth.append(sp.Ether()/f) + if os.fork() == 0: + time.sleep(1) + sp.sendp(eth, iface=send_if) + os._exit(0) + + ans = sp.sniff(iface=recv_if, timeout=3, filter= + "ip and src " + dst + " and dst " + src + " and icmp") + for a in ans: + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag == 0 and a.payload.flags == 0 and \ + sp.icmptypes[a.payload.payload.type] == 'echo-reply': + id = a.payload.payload.id + print("id=%#x" % (id)) + if id != eid: + print("WRONG ECHO REPLY ID") + exit(2) + data = a.payload.payload.payload.load + print("payload=%s" % (data)) + if data == payload: + exit(0) + print("PAYLOAD!=%s" % (payload)) + exit(1) + print("NO ECHO REPLY") + exit(2) + +if __name__ == '__main__': + main(send) diff --git a/tests/sys/netpfil/pf/frag-overhole.py b/tests/sys/netpfil/pf/frag-overhole.py new file mode 100644 index 000000000000..91697b6b83c6 --- /dev/null +++ b/tests/sys/netpfil/pf/frag-overhole.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# +# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org> + +from fragcommon import * + +# index boundary 4096 | +# |--------------| +# .... +# |--------------| +# |----------| +# |XXXX----------| +# |XXXX----| +# |---| + +# this should trigger "frag tail overlap %d" and "frag head overlap %d" +def send(src, dst, send_if, recv_if): + pid = os.getpid() + eid = pid & 0xffff + payload = b"ABCDEFGHIJKLMNOP" + dummy = b"01234567" + fragsize = 1024 + boundary = 4096 + fragnum = int(boundary / fragsize) + packet = sp.IP(src=src, dst=dst)/ \ + sp.ICMP(type='echo-request', id=eid)/ \ + ((int((boundary + fragsize) / len(payload)) + 1) * payload) + packet_length = len(packet) + frag = [] + fid = pid & 0xffff + for i in range(fragnum-1): + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(i * fragsize)>>3, flags='MF')/ + bytes(packet)[20 + i * fragsize:20 + (i + 1) * fragsize]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - fragsize) >> 3, flags='MF')/ + bytes(packet)[20 + boundary - fragsize:20 + boundary - len(dummy)]) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - len(dummy)) >> 3, flags='MF')/ + (dummy+bytes(packet)[20 + boundary:20 + boundary + fragsize])) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary - 8 - len(dummy)) >> 3, flags='MF')/ + (dummy+bytes(packet)[20 + boundary - 8:20 + boundary])) + frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid, + frag=(boundary + fragsize) >> 3)/bytes(packet)[20 + boundary + fragsize:]) + eth=[] + for f in frag: + eth.append(sp.Ether() / f) + + if os.fork() == 0: + time.sleep(1) + for e in eth: + sp.sendp(e, iface=send_if) + time.sleep(0.001) + os._exit(0) + + ans = sp.sniff(iface=recv_if, timeout=3, filter= + "ip and src " + dst + " and dst " + src + " and icmp") + for a in ans: + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag == 0 and \ + sp.icmptypes[a.payload.payload.type] == 'echo-reply': + id = a.payload.payload.id + print("id=%#x" % (id)) + if id != eid: + print("WRONG ECHO REPLY ID") + exit(2) + if a and a.type == sp.ETH_P_IP and \ + a.payload.proto == 1 and \ + a.payload.frag > 0 and \ + a.payload.flags == '': + length = (a.payload.frag << 3) + a.payload.len + print("len=%d" % (length)) + if length != packet_length: + print("WRONG ECHO REPLY LENGTH") + exit(1) + exit(0) + print("NO ECHO REPLY") + exit(1) + +if __name__ == '__main__': + main(send) diff --git a/tests/sys/netpfil/pf/frag6.py b/tests/sys/netpfil/pf/frag6.py index 28b1829d418c..26ae7af7c90c 100644 --- a/tests/sys/netpfil/pf/frag6.py +++ b/tests/sys/netpfil/pf/frag6.py @@ -1,25 +1,13 @@ import pytest import logging -import threading -import time +import random logging.getLogger("scapy").setLevel(logging.CRITICAL) +from utils import DelayedSend from atf_python.sys.net.tools import ToolsHelper from atf_python.sys.net.vnet import VnetTestTemplate -class DelayedSend(threading.Thread): - def __init__(self, packet): - threading.Thread.__init__(self) - self._packet = packet - - self.start() - - def run(self): - import scapy.all as sp - time.sleep(1) - sp.send(self._packet) - class TestFrag6(VnetTestTemplate): - REQUIRED_MODULES = ["pf"] + REQUIRED_MODULES = ["pf", "dummymbuf"] TOPOLOGY = { "vnet1": {"ifaces": ["if1"]}, "vnet2": {"ifaces": ["if1"]}, @@ -27,18 +15,22 @@ class TestFrag6(VnetTestTemplate): } def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ - "scrub fragment reassemble", + "scrub fragment reassemble min-ttl 10", "pass", "block in inet6 proto icmp6 icmp6-type echoreq", ]) + ToolsHelper.print_output("/sbin/pfilctl link -i dummymbuf:inet6 inet6") + ToolsHelper.print_output("/sbin/sysctl net.dummymbuf.rules=\"inet6 in %s enlarge 3000;\"" % ifname) def check_ping_reply(self, packet): print(packet) return False @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_dup_frag_hdr(self): "Test packets with duplicate fragment headers" srv_vnet = self.vnet_map["vnet2"] @@ -58,3 +50,227 @@ class TestFrag6(VnetTestTemplate): timeout=3) for p in packets: assert not p.getlayer(sp.ICMPv6EchoReply) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_overlong(self): + "Test overly long fragmented packet" + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + curr = 0 + pkts = [] + + frag_id = random.randint(0,0xffffffff) + gran = 1200 + + i = 0 + while curr <= 65535: + ipv61 = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") + more = True + g = gran + if curr + gran > 65535: + more = False + g = 65530 - curr + if i == 0: + pkt = ipv61 / sp.IPv6ExtHdrHopByHop(options=[sp.PadN(optlen=2), sp.Pad1()]) / \ + sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g) + else: + pkt = ipv61 / sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g) + pkts.append(pkt) + curr += gran + i += 1 + + 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 allow-opts", + ]) + + @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 = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble", + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_overlap(self): + "Ensure we discard packets with overlapping fragments" + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + packet = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f00f') * 90)) + frags = sp.fragment6(packet, 128) + assert len(frags) == 3 + + f = frags[0].getlayer(sp.IPv6ExtHdrFragment) + # Fragment with overlap + overlap = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \ + / sp.IPv6ExtHdrFragment(offset = 4, m = 1, id = f.id, nh = f.nh) \ + / sp.raw(bytes.fromhex('f00f') * 4) + frags = [ frags[0], frags[1], overlap, frags[2] ] + + # Delay the send so the sniffer is running when we transmit. + s = DelayedSend(frags) + + packets = sp.sniff(iface=self.vnet.iface_alias_map["if1"].name, + timeout=3) + for p in packets: + p.show() + assert not p.getlayer(sp.ICMPv6EchoReply) + +class TestFrag6_RouteTo(VnetTestTemplate): + REQUIRED_MODULES = ["pf"] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + "if2": {"prefixes6": [("2001:db8:1::1/64", "2001:db8:1::2/64")]}, + } + + def vnet2_handler(self, vnet): + if2name = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble", + "pass in route-to (%s 2001:db8:1::2) from 2001:db8::1 to 2001:db8:666::1" % if2name, + ]) + + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1300" % if2name) + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + + def vnet3_handler(self, vnet): + pass + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_big(self): + ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + pkt = sp.IPv6(dst="2001:db8:666::1") \ + / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 3000)) + frags = sp.fragment6(pkt, 1320) + + reply = sp.sr1(frags, timeout=3) + if reply: + reply.show() + + assert reply + + ip6 = reply.getlayer(sp.IPv6) + icmp6 = reply.getlayer(sp.ICMPv6PacketTooBig) + err_ip6 = reply.getlayer(sp.IPerror6) + + assert ip6 + assert ip6.src == "2001:db8::2" + assert ip6.dst == "2001:db8::1" + assert icmp6 + assert icmp6.mtu == 1300 + assert err_ip6 + assert err_ip6.src == "2001:db8::1" + assert err_ip6.dst == "2001:db8:666::1" diff --git a/tests/sys/netpfil/pf/fragmentation_compat.sh b/tests/sys/netpfil/pf/fragmentation_compat.sh index 21ce6b734ea1..1f4550ebd69e 100644 --- a/tests/sys/netpfil/pf/fragmentation_compat.sh +++ b/tests/sys/netpfil/pf/fragmentation_compat.sh @@ -74,7 +74,7 @@ v6_head() { atf_set descr 'IPv6 fragmentation test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -230,7 +230,7 @@ overreplace_head() { atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overreplace_body() @@ -248,7 +248,7 @@ overindex_head() { atf_set descr 'ping fragment that overlaps the first fragment at index boundary' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overindex_body() @@ -266,7 +266,7 @@ overlimit_head() { atf_set descr 'ping fragment at index boundary that cannot be requeued' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overlimit_body() @@ -326,53 +326,6 @@ reassemble_cleanup() pft_cleanup } -atf_test_case "no_df" "cleanup" -no_df_head() -{ - atf_set descr 'Test removing of DF flag' - atf_set require.user root -} - -no_df_body() -{ - setup_router_server_ipv4 - - ifconfig ${epair_tester}a mtu 9000 - jexec router ifconfig ${epair_tester}b mtu 9000 - jexec router ifconfig ${epair_server}a mtu 1500 - jexec server ifconfig ${epair_server}b mtu 1500 - - # Sanity check. - ping_server_check_reply exit:0 --ping-type=icmp - - pft_set_rules router \ - "scrub fragment reassemble" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 - - # Ping with non-fragmentable packets, this will fail. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF - - pft_set_rules router \ - "scrub any reassemble" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with non-fragmentable packets again. - # This time pf will strip the DF flag. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF -} - -no_df_cleanup() -{ - pft_cleanup -} - atf_init_test_cases() { atf_add_test_case "too_many_fragments" diff --git a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh index fb5c15ac2ff8..7cab89f5debb 100644 --- a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh +++ b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh @@ -32,7 +32,7 @@ match_full_v4_head() { atf_set descr 'Matching non-fragmented IPv4 packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } match_full_v4_body() @@ -63,7 +63,7 @@ match_fragment_v4_head() { atf_set descr 'Matching fragmented IPv4 packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } match_fragment_v4_body() @@ -93,7 +93,7 @@ compat_override_v4_head() { atf_set descr 'Scrub rules override "set reassemble" for IPv4' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } compat_override_v4_body() diff --git a/tests/sys/netpfil/pf/fragmentation_pass.sh b/tests/sys/netpfil/pf/fragmentation_pass.sh index d505accba5f2..5deaba18301d 100644 --- a/tests/sys/netpfil/pf/fragmentation_pass.sh +++ b/tests/sys/netpfil/pf/fragmentation_pass.sh @@ -77,7 +77,7 @@ v6_head() { atf_set descr 'IPv6 fragmentation test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -155,6 +155,75 @@ v6_cleanup() pft_cleanup } +atf_test_case "v6_route_to" "cleanup" +v6_route_to_head() +{ + atf_set descr 'Test IPv6 reassembly combined with route-to' + atf_set require.user root +} + +v6_route_to_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + epair_link=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair_send}b ${epair_link}a + vnet_mkjail singsing ${epair_link}b + + ifconfig ${epair_send}a inet6 2001:db8:42::1/64 no_dad up + + jexec alcatraz ifconfig ${epair_send}b inet6 2001:db8:42::2/64 no_dad up + jexec alcatraz ifconfig ${epair_link}a inet6 2001:db8:43::2/64 no_dad up + jexec alcatraz sysctl net.inet6.ip6.forwarding=1 + + jexec singsing ifconfig ${epair_link}b inet6 2001:db8:43::3/64 no_dad up + jexec singsing route add -6 2001:db8:42::/64 2001:db8:43::2 + route add -6 2001:db8:43::/64 2001:db8:42::2 + + jexec alcatraz ifconfig ${epair_send}b inet6 -ifdisabled + jexec alcatraz ifconfig ${epair_link}a inet6 -ifdisabled + jexec singsing ifconfig ${epair_link}b inet6 -ifdisabled + ifconfig ${epair_send}a inet6 -ifdisabled + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "set reassemble yes" \ + "pass" \ + "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state" + + # Forwarding test + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:43::3 + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 -s 4500 2001:db8:43::3 + + atf_check -s exit:0 -o ignore\ + ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3 + + # Now test this without fragmentation + pft_set_rules alcatraz \ + "set reassemble no" \ + "pass" \ + "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state" + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:43::3 + + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 -s 4500 2001:db8:43::3 + + atf_check -s exit:0 -o ignore\ + ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3 +} + +v6_route_to_cleanup() +{ + pft_cleanup +} + atf_test_case "mtu_diff" "cleanup" mtu_diff_head() { @@ -236,7 +305,7 @@ overreplace_head() { atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overreplace_body() @@ -254,7 +323,7 @@ overindex_head() { atf_set descr 'ping fragment that overlaps the first fragment at index boundary' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overindex_body() @@ -272,7 +341,7 @@ overlimit_head() { atf_set descr 'ping fragment at index boundary that cannot be requeued' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } overlimit_body() @@ -285,6 +354,42 @@ overlimit_cleanup() pft_cleanup } +atf_test_case "overhole" "cleanup" +overhole_head() +{ + atf_set descr 'ping fragment at index boundary which modifies pf hole counter' + atf_set require.user root + atf_set require.progs python3 scapy +} + +overhole_body() +{ + frag_common overhole +} + +overhole_cleanup() +{ + pft_cleanup +} + +atf_test_case "adjhole" "cleanup" +adjhole_head() +{ + atf_set descr 'overlapping ping fragments which modifies pf hole counter' + atf_set require.user root + atf_set require.progs python3 scapy +} + +adjhole_body() +{ + frag_common adjhole +} + +adjhole_cleanup() +{ + pft_cleanup +} + atf_test_case "reassemble" "cleanup" reassemble_head() { @@ -338,61 +443,7 @@ no_df_head() { atf_set descr 'Test removing of DF flag' atf_set require.user root -} - -no_df_body() -{ - setup_router_server_ipv4 - - ifconfig ${epair_tester}a mtu 9000 - jexec router ifconfig ${epair_tester}b mtu 9000 - jexec router ifconfig ${epair_server}a mtu 1500 - jexec server ifconfig ${epair_server}b mtu 1500 - - # Sanity check. - ping_server_check_reply exit:0 --ping-type=icmp - - pft_set_rules router \ - "set reassemble no" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 - - pft_set_rules router \ - "set reassemble yes" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with normal, fragmentable packets. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 - - # Ping with non-fragmentable packets. - ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF - - pft_set_rules router \ - "set reassemble yes no-df" \ - "pass out" \ - "block in" \ - "pass in inet proto icmp all icmp-type echoreq" - - # Ping with non-fragmentable packets again. - # This time pf will strip the DF flag. - ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF -} -no_df_cleanup() -{ - pft_cleanup -} - -atf_test_case "no_df" "cleanup" -no_df_head() -{ - atf_set descr 'Test removing of DF flag' - atf_set require.user root + atf_set require.progs python3 scapy } no_df_body() @@ -420,6 +471,7 @@ no_df_body() # getting properly forwarded. ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF } + no_df_cleanup() { pft_cleanup @@ -430,6 +482,7 @@ reassemble_slowpath_head() { atf_set descr 'Test reassembly on the slow path' atf_set require.user root + atf_set require.progs python3 scapy } reassemble_slowpath_body() @@ -553,17 +606,63 @@ dummynet_nat_cleanup() pft_cleanup } +atf_test_case "dummynet_fragmented" "cleanup" +dummynet_fragmented_head() +{ + atf_set descr 'Test dummynet on NATed fragmented traffic' + atf_set require.user root + atf_set require.progs python3 scapy +} + +dummynet_fragmented_body() +{ + pft_init + dummynet_init + + # No test for IPv6. IPv6 fragment reassembly can't be disabled. + setup_router_dummy_ipv4 + + jexec router dnctl pipe 1 config delay 1 + + pft_set_rules router \ + "set reassemble no" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet proto udp dnpipe (1, 1)" \ + "pass out on ${epair_server}a inet proto udp" \ + + ping_dummy_check_request exit:0 --ping-type=udp --send-length=10000 --send-frag-length=1280 + + rules=$(mktemp) || exit 1 + jexec router pfctl -qvsr | normalize_pfctl_s > $rules + + # Count that fragmented packets have hit the rule only once and that + # they have not created states. There is no stateful firewall support + # for fragmented packets. + grep -qE 'pass in on epair0b inet proto udp all keep state dnpipe\(1, 1\) .* Packets: 8 Bytes: 10168 States: 0 ' $rules || + atf_fail "Fragmented packets not counted correctly" +} + +dummynet_fragmented_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "too_many_fragments" atf_add_test_case "v6" + atf_add_test_case "v6_route_to" atf_add_test_case "mtu_diff" atf_add_test_case "overreplace" atf_add_test_case "overindex" atf_add_test_case "overlimit" + atf_add_test_case "overhole" + atf_add_test_case "adjhole" atf_add_test_case "reassemble" atf_add_test_case "no_df" atf_add_test_case "reassemble_slowpath" atf_add_test_case "dummynet" atf_add_test_case "dummynet_nat" + atf_add_test_case "dummynet_fragmented" } diff --git a/tests/sys/netpfil/pf/get_state.sh b/tests/sys/netpfil/pf/get_state.sh index 35adf64f6b11..eb2bc854c800 100644 --- a/tests/sys/netpfil/pf/get_state.sh +++ b/tests/sys/netpfil/pf/get_state.sh @@ -33,7 +33,7 @@ many_head() { atf_set descr 'Test retrieving many states' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } many_body() diff --git a/tests/sys/netpfil/pf/header.py b/tests/sys/netpfil/pf/header.py new file mode 100644 index 000000000000..6832cfe6d42b --- /dev/null +++ b/tests/sys/netpfil/pf/header.py @@ -0,0 +1,217 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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 pytest +import re +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestHeader(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/usr/sbin/arp -s 198.51.100.3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_many(self): + "Verify that we drop packets with silly numbers of headers." + + sendif = self.vnet.iface_alias_map["if1"].name + recvif = self.vnet.iface_alias_map["if2"].name + gw_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif) + gw_mac = re.sub("0a$", "0b", gw_mac) + + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Sanity check, ensure we get replies to normal ping + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") \ + / sp.ICMP(type='echo-request') + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + icmp = r.getlayer(sp.ICMP) + if not icmp: + continue + assert icmp.type == 8 # 'echo-request' + found = True + assert found + + # Up to 19 AH headers will pass + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") + for i in range(0, 18): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request') + + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert found + + # But more will get dropped + pkt = sp.Ether(dst=gw_mac) \ + / sp.IP(dst="198.51.100.3") + for i in range(0, 19): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request') + + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert not found + +class TestHeader6(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + SKIP_MODULES = [ "ipfilter" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]}, + "if2": {"prefixes6": [("2001:db8:1::2/64", "2001:db8:1::1/64")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "pass", + ]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_too_many(self): + "Verify that we drop packets with silly numbers of headers." + ToolsHelper.print_output("/sbin/ifconfig") + + sendif = self.vnet.iface_alias_map["if1"].name + recvif = self.vnet.iface_alias_map["if2"].name + our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif) + gw_mac = re.sub("0a$", "0b", our_mac) + + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Sanity check, ensure we get replies to normal ping + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") \ + / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + icmp = r.getlayer(sp.ICMPv6EchoRequest) + if not icmp: + continue + found = True + assert found + + # Up to 19 AH headers will pass + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") + for i in range(0, 18): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert found + + # But more will get dropped + pkt = sp.Ether(src=our_mac, dst=gw_mac) \ + / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") + for i in range(0, 19): + pkt = pkt / sp.AH(nh=51, payloadlen=1) + pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest() + s = DelayedSend(pkt, sendif) + reply = sp.sniff(iface=recvif, timeout=3) + print(reply) + + found = False + for r in reply: + r.show() + + ah = r.getlayer(sp.AH) + if not ah: + continue + found = True + assert not found diff --git a/tests/sys/netpfil/pf/icmp.py b/tests/sys/netpfil/pf/icmp.py new file mode 100644 index 000000000000..83096886691e --- /dev/null +++ b/tests/sys/netpfil/pf/icmp.py @@ -0,0 +1,254 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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 pytest +import subprocess +import re +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +def check(cmd): + ps = subprocess.Popen(cmd, shell=True) + ret = ps.wait() + if ret != 0: + raise Exception("Command %s returned %d" % (cmd, ret)) + +class TestICMP(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set reassemble yes", + "set state-policy if-bound", + "block", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % if2name) + + def vnet3_handler(self, vnet): + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + ifname = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname) + ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % ifname) + + def checkfn(packet): + icmp = packet.getlayer(sp.ICMP) + if not icmp: + return False + + if icmp.type != 3: + return False + + packet.show() + return True + + sp.sniff(iface=ifname, stop_filter=checkfn) + vnet.pipe.send("Got ICMP destination unreachable packet") + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_inner_match(self): + vnet = self.vnet_map["vnet1"] + dst_vnet = self.vnet_map["vnet3"] + sendif = vnet.iface_alias_map["if1"].name + + our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif) + dst_mac = re.sub("0a$", "0b", our_mac) + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Sanity check + check("/sbin/ping -c 1 192.0.2.1") + check("/sbin/ping -c 1 198.51.100.1") + check("/sbin/ping -c 2 198.51.100.3") + + # Establish a state + pkt = sp.Ether(src=our_mac, dst=dst_mac) \ + / sp.IP(src="192.0.2.2", dst="198.51.100.2") \ + / sp.ICMP(type='echo-request') \ + / "PAYLOAD" + sp.sendp(pkt, sendif, verbose=False) + + # Now try to pass an ICMP error message piggy-backing on that state, but + # use a different source address + pkt = sp.Ether(src=our_mac, dst=dst_mac) \ + / sp.IP(src="192.0.2.2", dst="198.51.100.3") \ + / sp.ICMP(type='dest-unreach') \ + / sp.IP(src="198.51.100.2", dst="192.0.2.2") \ + / sp.ICMP(type='echo-reply') + sp.sendp(pkt, sendif, verbose=False) + + try: + rcvd = self.wait_object(dst_vnet.pipe, timeout=1) + if rcvd: + raise Exception(rcvd) + except TimeoutError as e: + # We expect the timeout here. It means we didn't get the destination + # unreachable packet in vnet3 + pass + + def check_icmp_echo(self, sp, payload_size): + packet = sp.IP(dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request') \ + / sp.raw(bytes.fromhex('f0') * payload_size) + + p = sp.sr1(packet, iface=self.vnet.iface_alias_map["if1"].name, + timeout=3) + p.show() + + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + + if payload_size > 1464: + # Expect ICMP destination unreachable, fragmentation needed + assert ip.src == "192.0.2.1" + assert ip.dst == "192.0.2.2" + assert icmp.type == 3 # dest-unreach + assert icmp.code == 4 + assert icmp.nexthopmtu == 1492 + else: + # Expect echo reply + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert icmp.type == 0 # "echo-reply" + assert icmp.code == 0 + + return + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_fragmentation_needed(self): + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2") + ToolsHelper.print_output("/sbin/ping -c 1 -D -s 1472 198.51.100.2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + self.check_icmp_echo(sp, 128) + self.check_icmp_echo(sp, 1464) + self.check_icmp_echo(sp, 1468) + +class TestICMP_NAT(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set reassemble yes", + "set state-policy if-bound", + "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (if2name, if2name), + "block", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + def vnet3_handler(self, vnet): + ifname = vnet.iface_alias_map["if2"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_id_conflict(self): + """ + Test ICMP echo requests with the same ID from different clients. + Windows does this, and it can confuse pf. + """ + ifname = self.vnet.iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.3/24" % ifname) + + ToolsHelper.print_output("/sbin/ping -c 1 192.0.2.1") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.1") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2") + ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.3") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Address one + packet = sp.IP(src="192.0.2.2", dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request', id=42) \ + / sp.raw(bytes.fromhex('f0') * 16) + + p = sp.sr1(packet, timeout=3) + p.show() + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + assert ip.dst == "192.0.2.2" + assert icmp.id == 42 + + # Address one + packet = sp.IP(src="192.0.2.3", dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='echo-request', id=42) \ + / sp.raw(bytes.fromhex('f0') * 16) + + p = sp.sr1(packet, timeout=3) + p.show() + ip = p.getlayer(sp.IP) + icmp = p.getlayer(sp.ICMP) + assert ip + assert icmp + assert ip.dst == "192.0.2.3" + assert icmp.id == 42 diff --git a/tests/sys/netpfil/pf/icmp.sh b/tests/sys/netpfil/pf/icmp.sh index 72b531b08c51..279e3c7a29d5 100644 --- a/tests/sys/netpfil/pf/icmp.sh +++ b/tests/sys/netpfil/pf/icmp.sh @@ -33,7 +33,7 @@ cve_2019_5598_head() { atf_set descr 'Test CVE-2019-5598' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } cve_2019_5598_body() @@ -71,7 +71,74 @@ cve_2019_5598_cleanup() pft_cleanup } +atf_test_case "ttl_exceeded" "cleanup" +ttl_exceeded_head() +{ + atf_set descr 'Test that we correctly translate TTL exceeded back' + atf_set require.user root +} + +ttl_exceeded_body() +{ + pft_init + + epair_srv=$(vnet_mkepair) + epair_int=$(vnet_mkepair) + epair_cl=$(vnet_mkepair) + + vnet_mkjail srv ${epair_srv}a + jexec srv ifconfig ${epair_srv}a 192.0.2.1/24 up + jexec srv route add default 192.0.2.2 + + vnet_mkjail int ${epair_srv}b ${epair_int}a + jexec int sysctl net.inet.ip.forwarding=1 + jexec int ifconfig ${epair_srv}b 192.0.2.2/24 up + jexec int ifconfig ${epair_int}a 203.0.113.2/24 up + + vnet_mkjail nat ${epair_int}b ${epair_cl}b + jexec nat ifconfig ${epair_int}b 203.0.113.1/24 up + jexec nat ifconfig ${epair_cl}b 198.51.100.2/24 up + jexec nat sysctl net.inet.ip.forwarding=1 + jexec nat route add default 203.0.113.2 + + vnet_mkjail cl ${epair_cl}a + jexec cl ifconfig ${epair_cl}a 198.51.100.1/24 up + jexec cl route add default 198.51.100.2 + + jexec nat pfctl -e + pft_set_rules nat \ + "nat on ${epair_int}b from 198.51.100.0/24 -> (${epair_int}b)" \ + "block" \ + "pass inet proto udp" \ + "pass inet proto icmp icmp-type { echoreq }" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 203.0.113.1 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 203.0.113.2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 192.0.2.1 + + echo "UDP" + atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \ + jexec cl traceroute 192.0.2.1 + jexec nat pfctl -Fs + + echo "ICMP" + atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \ + jexec cl traceroute -I 192.0.2.1 +} + +ttl_exceeded_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "cve_2019_5598" + atf_add_test_case "ttl_exceeded" } diff --git a/tests/sys/netpfil/pf/icmp6.sh b/tests/sys/netpfil/pf/icmp6.sh new file mode 100644 index 000000000000..c55af906e3a6 --- /dev/null +++ b/tests/sys/netpfil/pf/icmp6.sh @@ -0,0 +1,204 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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. + +. $(atf_get_srcdir)/utils.subr + +common_dir=$(atf_get_srcdir)/../common + +atf_test_case "zero_id" "cleanup" +zero_id_head() +{ + atf_set descr 'Test ICMPv6 echo with ID 0 keep being blocked' + atf_set require.user root + atf_set require.progs python3 scapy +} + +zero_id_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 2001:db8::1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "set block-policy drop" \ + "antispoof quick for { egress ${epair}b }" \ + "block all" \ + "pass out" \ + "pass in quick inet6 proto IPV6-ICMP icmp6-type 135" \ + "pass in quick inet6 proto IPV6-ICMP icmp6-type 136" \ + "pass out quick inet6 proto IPV6 from self to any" + + # Now we can't ping + atf_check -s exit:2 -o ignore \ + ping -c 1 2001:db8::1 + + # Force neighbour discovery + ndp -d 2001:db8::1 + + # Verify that we don't confuse echo request with ID 0 for neighbour discovery + atf_check -s exit:1 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8::1 \ + --replyif ${epair}a + + jexec alcatraz pfctl -ss -vv + jexec alcatraz pfctl -sr -vv +} + +zero_id_cleanup() +{ + pft_cleanup +} + +atf_test_case "ttl_exceeded" "cleanup" +ttl_exceeded_head() +{ + atf_set descr 'Test that we correctly translate TTL exceeded back' + atf_set require.user root +} + +ttl_exceeded_body() +{ + pft_init + + epair_srv=$(vnet_mkepair) + epair_int=$(vnet_mkepair) + epair_cl=$(vnet_mkepair) + + vnet_mkjail srv ${epair_srv}a + jexec srv ifconfig ${epair_srv}a inet6 2001:db8:1::1/64 no_dad up + jexec srv route add -6 default 2001:db8:1::2 + + vnet_mkjail int ${epair_srv}b ${epair_int}a + jexec int sysctl net.inet6.ip6.forwarding=1 + jexec int ifconfig ${epair_srv}b inet6 2001:db8:1::2/64 no_dad up + jexec int ifconfig ${epair_int}a inet6 2001:db8:2::2/64 no_dad up + + vnet_mkjail nat ${epair_int}b ${epair_cl}b + jexec nat ifconfig ${epair_int}b inet6 2001:db8:2::1 no_dad up + jexec nat ifconfig ${epair_cl}b inet6 2001:db8:3::2/64 no_dad up + jexec nat sysctl net.inet6.ip6.forwarding=1 + jexec nat route add -6 default 2001:db8:2::2 + + vnet_mkjail cl ${epair_cl}a + jexec cl ifconfig ${epair_cl}a inet6 2001:db8:3::1/64 no_dad up + jexec cl route add -6 default 2001:db8:3::2 + + jexec nat pfctl -e + pft_set_rules nat \ + "nat on ${epair_int}b from 2001:db8:3::/64 -> (${epair_int}b:0)" \ + "block" \ + "pass inet6 proto udp" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv, echoreq }" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:3::2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:2::1 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:2::2 + atf_check -s exit:0 -o ignore \ + jexec cl ping -c 1 2001:db8:1::1 + + echo "UDP" + atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \ + jexec cl traceroute6 2001:db8:1::1 + jexec nat pfctl -Fs + + echo "ICMP" + atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \ + jexec cl traceroute6 -I 2001:db8:1::1 +} + +ttl_exceeded_cleanup() +{ + pft_cleanup +} + +atf_test_case "repeat" "cleanup" +repeat_head() +{ + atf_set descr 'Ensure that repeated NDs work' + atf_set require.user root + atf_set require.progs ndisc6 +} + +repeat_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 2001:db8::1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block all" \ + "pass quick inet6 proto ipv6-icmp all icmp6-type neighbrsol keep state (if-bound) ridentifier 1000000107" + + jexec alcatraz pfctl -x loud + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv + atf_check -s exit:0 -o ignore \ + ndisc6 -m -n -r 1 2001:db8::1 ${epair}a + jexec alcatraz pfctl -ss -vv +} + +repeat_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "zero_id" + atf_add_test_case "ttl_exceeded" + atf_add_test_case "repeat" +} diff --git a/tests/sys/netpfil/pf/if_enc.sh b/tests/sys/netpfil/pf/if_enc.sh new file mode 100644 index 000000000000..40090b175470 --- /dev/null +++ b/tests/sys/netpfil/pf/if_enc.sh @@ -0,0 +1,178 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +# +# The following network is used as a base for testing. +# +# +# ${awan}b |----------| ${bwan}b +# 2.0.0.1 | host wan | 3.0.0.1 +# .---->| Internet |<----. +# A WAN | |----------| | B WAN +# | | +# Office A side | | Office B side +# | ${awan}a ${bwan}a | +# v 2.0.0.22 3.0.0.33 v +# ${alan}b |----------| |----------| ${blan}b +# 1.0.0.1 | host agw | | host bgw | 4.0.0.1 +# .----------->| gateway | < IPsec > | gateway |<-----------. +# | A LAN |----------| tunnel |----------| B LAN | +# | | +# | | +# | ${alan}a ${blan}a | +# v 1.0.0.11 4.0.0.44 v +# |----------| |----------| +# | host a | | host b | +# | client | | client | +# |----------| |----------| +# +# +# There is routing between office A clients and office B ones. The traffic is +# encrypted, i.e. host wan should see IPsec flow (ESP packets). +# + +ipsec_init() +{ + if ! sysctl -q kern.features.ipsec >/dev/null ; then + atf_skip "This test requires ipsec" + fi +} + +if_enc_init() +{ + ipsec_init + if ! kldstat -q -m if_enc; then + atf_skip "This test requires if_enc" + fi +} + +build_test_network() +{ + alan=$(vnet_mkepair) + awan=$(vnet_mkepair) + bwan=$(vnet_mkepair) + blan=$(vnet_mkepair) + + # host a + vnet_mkjail a ${alan}a + jexec a ifconfig ${alan}a 1.0.0.11/24 up + jexec a route add default 1.0.0.1 + + # host agw + vnet_mkjail agw ${alan}b ${awan}a + jexec agw ifconfig ${alan}b 1.0.0.1/24 up + jexec agw ifconfig ${awan}a 2.0.0.22/24 up + jexec agw route add default 2.0.0.1 + jexec agw sysctl net.inet.ip.forwarding=1 + + # host wan + vnet_mkjail wan ${awan}b ${bwan}b + jexec wan ifconfig ${awan}b 2.0.0.1/24 up + jexec wan ifconfig ${bwan}b 3.0.0.1/24 up + jexec wan sysctl net.inet.ip.forwarding=1 + + # host bgw + vnet_mkjail bgw ${bwan}a ${blan}b + jexec bgw ifconfig ${bwan}a 3.0.0.33/24 up + jexec bgw ifconfig ${blan}b 4.0.0.1/24 up + jexec bgw route add default 3.0.0.1 + jexec bgw sysctl net.inet.ip.forwarding=1 + + # host b + vnet_mkjail b ${blan}a + jexec b ifconfig ${blan}a 4.0.0.44/24 up + jexec b route add default 4.0.0.1 + + # Office A VPN setup + echo ' + spdadd 1.0.0.0/24 4.0.0.0/24 any -P out ipsec esp/tunnel/2.0.0.22-3.0.0.33/require; + spdadd 4.0.0.0/24 1.0.0.0/24 any -P in ipsec esp/tunnel/3.0.0.33-2.0.0.22/require; + add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456"; + add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456"; + ' | jexec agw setkey -c + + # Office B VPN setup + echo ' + spdadd 4.0.0.0/24 1.0.0.0/24 any -P out ipsec esp/tunnel/3.0.0.33-2.0.0.22/require; + spdadd 1.0.0.0/24 4.0.0.0/24 any -P in ipsec esp/tunnel/2.0.0.22-3.0.0.33/require; + add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456"; + add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456"; + ' | jexec bgw setkey -c +} + +atf_test_case "ip4_pfil_in_after_stripping" "cleanup" +ip4_pfil_in_after_stripping_head() +{ + atf_set descr 'Test that pf pulls up mbuf if m_len==0 after stripping the outer header' + atf_set require.user root + atf_set require.progs nc +} +ip4_pfil_in_after_stripping_body() +{ + pft_init + if_enc_init + + build_test_network + + # Sanity check + atf_check -s exit:0 -o ignore jexec a ping -c3 4.0.0.44 + + # Configure port forwarding on host bgw + jexec bgw ifconfig enc0 up + jexec bgw sysctl net.inet.ipsec.filtertunnel=0 + jexec bgw sysctl net.enc.in.ipsec_filter_mask=2 # after stripping + jexec bgw sysctl net.enc.out.ipsec_filter_mask=1 # before outer header + jexec bgw pfctl -e + pft_set_rules bgw \ + "rdr on enc0 proto tcp to 4.0.0.1 port 666 -> 4.0.0.44" \ + "pass" + + # Prepare the catcher on host b + echo "unexpected" > ./receiver + jexec b nc -n4l -N 666 > ./receiver & + nc_pid=$! + sleep 1 + + # Poke it from host a to host bgw + spell="Ak Ohum Oktay Weez Barsoom." + echo $spell | jexec a nc -w3 4.0.0.1 666 + + # Expect it to hit host b instead + sleep 1 # let the catcher finish + jexec b kill -KILL $nc_pid # in a fail case the catcher may listen forever + atf_check_equal "$spell" "$(cat ./receiver)" +} +ip4_pfil_in_after_stripping_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "ip4_pfil_in_after_stripping" +} diff --git a/tests/sys/netpfil/pf/ioctl/Makefile b/tests/sys/netpfil/pf/ioctl/Makefile index 5d4e9dbcff5f..6bcf48432d30 100644 --- a/tests/sys/netpfil/pf/ioctl/Makefile +++ b/tests/sys/netpfil/pf/ioctl/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/netpfil/pf/ioctl diff --git a/tests/sys/netpfil/pf/killstate.sh b/tests/sys/netpfil/pf/killstate.sh index 046d640ed355..447a4e388f11 100644 --- a/tests/sys/netpfil/pf/killstate.sh +++ b/tests/sys/netpfil/pf/killstate.sh @@ -47,7 +47,7 @@ v4_head() { atf_set descr 'Test killing states by IPv4 address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -110,7 +110,7 @@ v6_head() { atf_set descr 'Test killing states by IPv6 address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -177,7 +177,7 @@ label_head() { atf_set descr 'Test killing states by label' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } label_body() @@ -241,7 +241,7 @@ multilabel_head() { atf_set descr 'Test killing states with multiple labels by label' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } multilabel_body() @@ -321,7 +321,7 @@ gateway_head() { atf_set descr 'Test killing states by route-to/reply-to address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } gateway_body() @@ -410,7 +410,7 @@ match_body() vnet_mkjail singsing ${epair_two}b jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up jexec singsing route add default 198.51.100.1 - jexec singsing /usr/sbin/inetd -p inetd-echo.pid \ + jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-echo.pid \ $(atf_get_srcdir)/echo_inetd.conf route add 198.51.100.0/24 192.0.2.2 @@ -462,7 +462,7 @@ interface_head() { atf_set descr 'Test killing states based on interface' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } interface_body() @@ -518,7 +518,7 @@ id_head() { atf_set descr 'Test killing states by id' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } id_body() @@ -574,12 +574,68 @@ id_cleanup() pft_cleanup } +atf_test_case "key" "cleanup" +key_head() +{ + atf_set descr 'Test killing states by their key' + atf_set require.user root + atf_set require.progs python3 scapy +} + +key_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "block all" \ + "pass in proto tcp" \ + "pass in proto icmp" + + # Sanity check & establish state + atf_check -s exit:0 -o ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 192.0.2.2 \ + --replyif ${epair}a + + # Get the state key + key=$(jexec alcatraz pfctl -ss -vvv | awk '/icmp/ { print($2 " " $3 " " $4 " " $5); }') + bad_key=$(echo ${key} | sed 's/icmp/tcp/') + + # Kill the wrong key + atf_check -s exit:0 -e "match:killed 0 states" \ + jexec alcatraz pfctl -k key -k "${bad_key}" + if ! find_state; + then + atf_fail "Killing a different ID removed the state." + fi + + # Kill the correct key + atf_check -s exit:0 -e "match:killed 1 states" \ + jexec alcatraz pfctl -k key -k "${key}" + if find_state; + then + atf_fail "Killing the state did not remove it." + fi +} + +key_cleanup() +{ + pft_cleanup +} + atf_test_case "nat" "cleanup" nat_head() { atf_set descr 'Test killing states by their NAT-ed IP address' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nat_body() @@ -653,5 +709,6 @@ atf_init_test_cases() atf_add_test_case "match" atf_add_test_case "interface" atf_add_test_case "id" + atf_add_test_case "key" atf_add_test_case "nat" } diff --git a/tests/sys/netpfil/pf/limits.sh b/tests/sys/netpfil/pf/limits.sh new file mode 100644 index 000000000000..69f0b6af2ccf --- /dev/null +++ b/tests/sys/netpfil/pf/limits.sh @@ -0,0 +1,119 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Kristof Provost <kp@FreeBSD.org> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Test setting and retrieving limits' + atf_set require.user root +} + +basic_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "set limit states 200" \ + "set limit frags 100" \ + "set limit src-nodes 50" \ + "set limit table-entries 25" + + atf_check -s exit:0 -o match:'states.*200' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'frags.*100' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'src-nodes.*50' \ + jexec alcatraz pfctl -sm + atf_check -s exit:0 -o match:'table-entries.*25' \ + jexec alcatraz pfctl -sm +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero" "cleanup" +zero_head() +{ + atf_set descr 'Test changing a limit from zero on an in-use zone' + atf_set require.user root +} + +zero_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + jexec alcatraz pfctl -e + # Set no limit + pft_set_rules noflush alcatraz \ + "set limit states 0" \ + "pass" + + # Check that we really report no limit + atf_check -s exit:0 -o 'match:states hard limit 0' \ + jexec alcatraz pfctl -sa + + # Create a state + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + # Limit states + pft_set_rules noflush alcatraz \ + "set limit states 1000" \ + "pass" + + # And create a new state + atf_check -s exit:0 -o ignore \ + ping -c 3 192.0.2.1 + + atf_check -s exit:0 -o 'match:states hard limit 1000' \ + jexec alcatraz pfctl -sa +} + +zero_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "zero" +} diff --git a/tests/sys/netpfil/pf/map_e.sh b/tests/sys/netpfil/pf/map_e.sh deleted file mode 100644 index 742264dcf547..000000000000 --- a/tests/sys/netpfil/pf/map_e.sh +++ /dev/null @@ -1,91 +0,0 @@ -# -# SPDX-License-Identifier: BSD-2-Clause -# -# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com> -# -# 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. - -. $(atf_get_srcdir)/utils.subr - -atf_test_case "map_e" "cleanup" -map_e_head() -{ - atf_set descr 'map-e-portset test' - atf_set require.user root -} - -map_e_body() -{ - NC_TRY_COUNT=12 - - pft_init - - epair_map_e=$(vnet_mkepair) - epair_echo=$(vnet_mkepair) - - vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a - vnet_mkjail echo ${epair_echo}b - - ifconfig ${epair_map_e}a 192.0.2.2/24 up - route add -net 198.51.100.0/24 192.0.2.1 - - jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up - jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up - jexec map_e sysctl net.inet.ip.forwarding=1 - - jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up - jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf - - # Enable pf! - jexec map_e pfctl -e - pft_set_rules map_e \ - "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342" - - # Only allow specified ports. - jexec echo pfctl -e - pft_set_rules echo "block return all" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ - "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ - "set skip on lo" - - i=0 - while [ ${i} -lt ${NC_TRY_COUNT} ] - do - echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 - if [ $? -ne 0 ]; then - atf_fail "nc failed (${i})" - fi - i=$((${i}+1)) - done -} - -map_e_cleanup() -{ - rm -f inetd-echo.pid - pft_cleanup -} - -atf_init_test_cases() -{ - atf_add_test_case "map_e" -} diff --git a/tests/sys/netpfil/pf/match.sh b/tests/sys/netpfil/pf/match.sh index bb088c5bf47c..58c1e021310a 100644 --- a/tests/sys/netpfil/pf/match.sh +++ b/tests/sys/netpfil/pf/match.sh @@ -26,6 +26,8 @@ . $(atf_get_srcdir)/utils.subr +common_dir=$(atf_get_srcdir)/../common + atf_test_case "dummynet" "cleanup" dummynet_head() { @@ -67,7 +69,117 @@ dummynet_cleanup() pft_cleanup } +atf_test_case "quick" "cleanup" +quick_head() +{ + atf_set descr 'Test quick on match rules' + atf_set require.user root +} + +quick_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}b + + ifconfig ${epair}a 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass" \ + "match in quick proto icmp" \ + "block" + + # 'match quick' should retain the previous pass/block state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + + pft_set_rules alcatraz \ + "block" \ + "match in quick proto icmp" \ + "pass" + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.2 +} + +quick_cleanup() +{ + pft_cleanup +} + +atf_test_case "allow_opts" "cleanup" +allow_opts_head() +{ + atf_set descr 'Test allowing IP options via match' + atf_set require.user root + atf_set require.progs python3 scapy +} + +allow_opts_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e + jexec alcatraz pfctl -x loud + pft_set_rules alcatraz \ + "match proto icmp allow-opts" \ + "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:0 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b + + # This doesn't work without 'allow-opts' + pft_set_rules alcatraz \ + "match proto icmp" \ + "pass" + atf_check -s exit:1 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b + + # Setting it on a pass rule still works. + pft_set_rules alcatraz \ + "pass allow-opts" + atf_check -s exit:0 -o ignore \ + ${common_dir}/pft_ping.py \ + --sendif ${epair}b \ + --to 192.0.2.1 \ + --send-nop \ + --replyif ${epair}b +} + +allow_opts_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "dummynet" + atf_add_test_case "quick" + atf_add_test_case "allow_opts" } diff --git a/tests/sys/netpfil/pf/max_pkt_rate.sh b/tests/sys/netpfil/pf/max_pkt_rate.sh new file mode 100644 index 000000000000..bdd140eb60dd --- /dev/null +++ b/tests/sys/netpfil/pf/max_pkt_rate.sh @@ -0,0 +1,121 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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. + +. $(atf_get_srcdir)/utils.subr + +common_setup() +{ + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e +} + +common_test() +{ + # One ping will pass + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # As will a second + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # But the third should fail + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + # But three seconds later we can ping again + sleep 3 + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 +} + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic maximum packet rate test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "block" \ + "pass in proto icmp max-pkt-rate 2/2" + + common_test +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "anchor" "cleanup" +anchor_head() +{ + atf_set descr 'maximum packet rate on anchor' + atf_set require.user root +} + +anchor_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\" proto icmp max-pkt-rate 2/2 {\n \ + pass \n \ + }" + + common_test +} + +anchor_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "anchor" +} diff --git a/tests/sys/netpfil/pf/max_pkt_size.sh b/tests/sys/netpfil/pf/max_pkt_size.sh new file mode 100644 index 000000000000..030d642303fc --- /dev/null +++ b/tests/sys/netpfil/pf/max_pkt_size.sh @@ -0,0 +1,122 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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. + +. $(atf_get_srcdir)/utils.subr + +common_setup() +{ + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e +} + +common_test() +{ + # Small packets pass + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 100 192.0.2.1 + + # Larger packets do not + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 101 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 128 192.0.2.1 +} + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic max-pkt-size test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "pass max-pkt-size 128" + + common_test + + # We can enforce this on fragmented packets too + pft_set_rules alcatraz \ + "pass max-pkt-size 2000" + + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1400 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -s 1972 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 -s 1973 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 3 -s 3000 192.0.2.1 +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "match" "cleanup" +match_head() +{ + atf_set descr 'max-pkt-size on match rules' + atf_set require.user root +} + +match_body() +{ + pft_init + + common_setup + + pft_set_rules alcatraz \ + "match in max-pkt-size 128" \ + "pass" + + common_test +} + +match_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "match" +} diff --git a/tests/sys/netpfil/pf/max_states.sh b/tests/sys/netpfil/pf/max_states.sh new file mode 100755 index 000000000000..1bf6814f9283 --- /dev/null +++ b/tests/sys/netpfil/pf/max_states.sh @@ -0,0 +1,62 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + + +max_states_head() +{ + atf_set descr 'Max states per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_states_body() +{ + setup_router_dummy_ipv6 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp keep state (max 3)" \ + "pass out on ${epair_server}a inet6 proto tcp keep state" + + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4201 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4202 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4203 + ping_dummy_check_request exit:1 --ping-type=tcpsyn --send-sport=4204 +} + +max_states_cleanup() +{ + pft_cleanup +} + + +atf_init_test_cases() +{ + atf_add_test_case "max_states" +} diff --git a/tests/sys/netpfil/pf/mbuf.sh b/tests/sys/netpfil/pf/mbuf.sh new file mode 100644 index 000000000000..d845f793a969 --- /dev/null +++ b/tests/sys/netpfil/pf/mbuf.sh @@ -0,0 +1,236 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +dummymbuf_init() +{ + if ! kldstat -q -m dummymbuf; then + atf_skip "This test requires dummymbuf" + fi +} + +atf_test_case "inet_in_mbuf_len" "cleanup" +inet_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip)' + atf_set require.user root +} +inet_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + # Should be allowed by from/to addresses + pft_set_rules alcatraz \ + "block" \ + "pass in from 192.0.2.1 to 192.0.2.2" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:inet inet + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=19 + # provided IPv4 basic header is 20 bytes long, it should impact the dst addr + jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 19;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +inet_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_test_case "inet6_in_mbuf_len" "cleanup" +inet6_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip6_hdr)' + atf_set require.user root +} +inet6_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a inet6 2001:db8::1/64 up no_dad + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b inet6 2001:db8::2/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 2001:db8::2 + + # Avoid redundant ICMPv6 packets to avoid false positives during + # counting of net.dummymbuf.hits. + ndp -i ${epair}a -- -nud + jexec alcatraz ndp -i ${epair}b -- -nud + + # Should be allowed by from/to addresses + pft_set_rules alcatraz \ + "block" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in inet6 from 2001:db8::1 to 2001:db8::2" + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:inet6 inet6 + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=39 + # provided IPv6 basic header is 40 bytes long, it should impact the dst addr + jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 39;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 2001:db8::2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +inet6_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_test_case "ethernet_in_mbuf_len" "cleanup" +ethernet_in_mbuf_len_head() +{ + atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ether_header)' + atf_set require.user root +} +ethernet_in_mbuf_len_body() +{ + pft_init + dummymbuf_init + + epair=$(vnet_mkepair) + epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }') + ifconfig ${epair}a 192.0.2.1/24 up + + # Set up a simple jail with one interface + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + epair_b_mac=$(jexec alcatraz ifconfig ${epair}b ether | awk '/ether/ { print $2; }') + + # Sanity check + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should be denied + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "ether block" \ + "pass" + atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2 + + # Should be allowed by from/to addresses + echo $epair_a_mac + echo $epair_b_mac + pft_set_rules alcatraz \ + "ether block" \ + "ether pass in from ${epair_a_mac} to ${epair_b_mac}" \ + "ether pass out from ${epair_b_mac} to ${epair_a_mac}" \ + "pass" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + + # Should still work for m_len=0 + jexec alcatraz pfilctl link -i dummymbuf:ethernet ethernet + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 0;" + atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=1 + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 1;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=11 + # for the simplest L2 Ethernet frame it should impact src field + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 11;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" + + # m_len=13 + # provided L2 Ethernet simplest header is 14 bytes long, it should impact ethertype field + jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 13;" + jexec alcatraz sysctl net.dummymbuf.hits=0 + atf_check -s exit:0 -o ignore ping -c1 192.0.2.2 + atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)" +} +ethernet_in_mbuf_len_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "inet_in_mbuf_len" + atf_add_test_case "inet6_in_mbuf_len" + atf_add_test_case "ethernet_in_mbuf_len" +} diff --git a/tests/sys/netpfil/pf/modulate.sh b/tests/sys/netpfil/pf/modulate.sh index 98d5df14a44d..1abe22cff391 100644 --- a/tests/sys/netpfil/pf/modulate.sh +++ b/tests/sys/netpfil/pf/modulate.sh @@ -31,7 +31,7 @@ modulate_v4_head() { atf_set descr 'IPv4 TCP sequence number modulation' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } modulate_v4_body() @@ -54,7 +54,7 @@ modulate_v6_head() { atf_set descr 'IPv6 TCP sequence number modulation' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } modulate_v6_body() diff --git a/tests/sys/netpfil/pf/nat.sh b/tests/sys/netpfil/pf/nat.sh index 7cc0d8f35c96..f1fdf6405d97 100644 --- a/tests/sys/netpfil/pf/nat.sh +++ b/tests/sys/netpfil/pf/nat.sh @@ -2,6 +2,8 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2018 Kristof Provost <kp@FreeBSD.org> +# Copyright (c) 2025 Kajetan Staszkiewicz <ks@FreeBSD.org> +# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +53,7 @@ exhaust_body() jexec nat sysctl net.inet.ip.forwarding=1 jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up - jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf + jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf # Enable pf! jexec nat pfctl -e @@ -79,7 +81,6 @@ exhaust_body() exhaust_cleanup() { - rm -f inetd-echo.pid pft_cleanup } @@ -113,13 +114,684 @@ nested_anchor_body() } +endpoint_independent_setup() +{ + pft_init + filter="udp and dst port 1234" # only capture udp pings + + epair_client=$(vnet_mkepair) + epair_nat=$(vnet_mkepair) + epair_server1=$(vnet_mkepair) + epair_server2=$(vnet_mkepair) + bridge=$(vnet_mkbridge) + + vnet_mkjail nat ${epair_client}b ${epair_nat}a + vnet_mkjail client ${epair_client}a + vnet_mkjail server1 ${epair_server1}a + vnet_mkjail server2 ${epair_server2}a + + ifconfig ${epair_server1}b up + ifconfig ${epair_server2}b up + ifconfig ${epair_nat}b up + ifconfig ${bridge} \ + addm ${epair_server1}b \ + addm ${epair_server2}b \ + addm ${epair_nat}b \ + up + + jexec nat ifconfig ${epair_client}b 192.0.2.1/24 up + jexec nat ifconfig ${epair_nat}a 198.51.100.42/24 up + jexec nat sysctl net.inet.ip.forwarding=1 + + jexec client ifconfig ${epair_client}a 192.0.2.2/24 up + jexec client route add default 192.0.2.1 + + jexec server1 ifconfig ${epair_server1}a 198.51.100.32/24 up + jexec server2 ifconfig ${epair_server2}a 198.51.100.22/24 up +} + +endpoint_independent_common() +{ + # Enable pf! + jexec nat pfctl -e + + # validate non-endpoint independent nat rule behaviour + pft_set_rules nat "${1}" + + jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ + --immediate-mode $filter & + server1tcppid="$!" + jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \ + --immediate-mode $filter & + server2tcppid="$!" + + # send out multiple packets + for i in $(seq 1 10); do + echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0 + echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0 + done + + kill $server1tcppid + kill $server2tcppid + + tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}') + tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}') + + if [ -z $tuple_server1 ] + then + atf_fail "server1 did not receive connection from client (default)" + fi + + if [ -z $tuple_server2 ] + then + atf_fail "server2 did not receive connection from client (default)" + fi + + if [ "$tuple_server1" = "$tuple_server2" ] + then + echo "server1 tcpdump: $tuple_server1" + echo "server2 tcpdump: $tuple_server2" + atf_fail "Received same IP:port on server1 and server2 (default)" + fi + + # validate endpoint independent nat rule behaviour + pft_set_rules nat "${2}" + + jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ + --immediate-mode $filter & + server1tcppid="$!" + jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \ + --immediate-mode $filter & + server2tcppid="$!" + + # send out multiple packets, sometimes one fails to go through + for i in $(seq 1 10); do + echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0 + echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0 + done + + kill $server1tcppid + kill $server2tcppid + + tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}') + tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}') + + if [ -z $tuple_server1 ] + then + atf_fail "server1 did not receive connection from client (endpoint-independent)" + fi + + if [ -z $tuple_server2 ] + then + atf_fail "server2 did not receive connection from client (endpoint-independent)" + fi + + if [ ! "$tuple_server1" = "$tuple_server2" ] + then + echo "server1 tcpdump: $tuple_server1" + echo "server2 tcpdump: $tuple_server2" + atf_fail "Received different IP:port on server1 than server2 (endpoint-independent)" + fi +} + +atf_test_case "endpoint_independent_compat" "cleanup" +endpoint_independent_compat_head() +{ + atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers' + atf_set require.user root +} + +endpoint_independent_compat_body() +{ + endpoint_independent_setup # Sets ${epair_…} variables + + endpoint_independent_common \ + "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a)" \ + "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a) endpoint-independent" +} + +endpoint_independent_compat_cleanup() +{ + pft_cleanup + rm -f server1.out + rm -f server2.out +} + +atf_test_case "endpoint_independent_pass" "cleanup" +endpoint_independent_pass_head() +{ + atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers' + atf_set require.user root +} + +endpoint_independent_pass_body() +{ + endpoint_independent_setup # Sets ${epair_…} variables + + endpoint_independent_common \ + "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) keep state" \ + "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) endpoint-independent keep state" + +} + +endpoint_independent_pass_cleanup() +{ + pft_cleanup + rm -f server1.out + rm -f server2.out +} + nested_anchor_cleanup() { pft_cleanup } +atf_test_case "nat6_nolinklocal" "cleanup" +nat6_nolinklocal_head() +{ + atf_set descr 'Ensure we do not use link-local addresses' + atf_set require.user root +} + +nat6_nolinklocal_body() +{ + pft_init + + epair_nat=$(vnet_mkepair) + epair_echo=$(vnet_mkepair) + + vnet_mkjail nat ${epair_nat}b ${epair_echo}a + vnet_mkjail echo ${epair_echo}b + + ifconfig ${epair_nat}a inet6 2001:db8::2/64 no_dad up + route add -6 -net 2001:db8:1::/64 2001:db8::1 + + jexec nat ifconfig ${epair_nat}b inet6 2001:db8::1/64 no_dad up + jexec nat ifconfig ${epair_echo}a inet6 2001:db8:1::1/64 no_dad up + jexec nat sysctl net.inet6.ip6.forwarding=1 + + jexec echo ifconfig ${epair_echo}b inet6 2001:db8:1::2/64 no_dad up + # Ensure we can't reply to link-local pings + jexec echo pfctl -e + pft_set_rules echo \ + "pass" \ + "block in inet6 proto icmp6 from fe80::/10 to any icmp6-type echoreq" + + jexec nat pfctl -e + pft_set_rules nat \ + "nat pass on ${epair_echo}a inet6 from 2001:db8::/64 to any -> (${epair_echo}a)" \ + "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8::1 + for i in `seq 0 10` + do + atf_check -s exit:0 -o ignore \ + ping -6 -c 1 2001:db8:1::2 + done +} + +nat6_nolinklocal_cleanup() +{ + pft_cleanup +} + +empty_table_common() +{ + option=$1 + + pft_init + + epair_wan=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + vnet_mkjail srv ${epair_wan}a + jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up + + vnet_mkjail rtr ${epair_wan}b ${epair_lan}a + jexec rtr ifconfig ${epair_wan}b 192.0.2.1/24 up + jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + + ifconfig ${epair_lan}b 198.51.100.2/24 up + route add default 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "table <empty>" \ + "nat on ${epair_wan}b inet from 198.51.100.0/24 -> <empty> ${option}" \ + "pass" + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # Provoke divide by zero + ping -c 1 192.0.2.2 + true +} + +atf_test_case "empty_table_source_hash" "cleanup" +empty_table_source_hash_head() +{ + atf_set descr 'Test source-hash on an emtpy table' + atf_set require.user root +} + +empty_table_source_hash_body() +{ + empty_table_common "source-hash" +} + +empty_table_source_hash_cleanup() +{ + pft_cleanup +} + +atf_test_case "empty_table_random" "cleanup" +empty_table_random_head() +{ + atf_set descr 'Test random on an emtpy table' + atf_set require.user root +} + +empty_table_random_body() +{ + empty_table_common "random" +} + +empty_table_random_cleanup() +{ + pft_cleanup +} + +no_addrs_common() +{ + option=$1 + + pft_init + + epair_wan=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + vnet_mkjail srv ${epair_wan}a + jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up + + vnet_mkjail rtr ${epair_wan}b ${epair_lan}a + jexec rtr route add -net 192.0.2.0/24 -iface ${epair_wan}b + jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + + ifconfig ${epair_lan}b 198.51.100.2/24 up + route add default 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "nat on ${epair_wan}b inet from 198.51.100.0/24 -> (${epair_wan}b) ${option}" \ + "pass" + + # Provoke divide by zero + ping -c 1 192.0.2.2 + true +} + +atf_test_case "no_addrs_source_hash" "cleanup" +no_addrs_source_hash_head() +{ + atf_set descr 'Test source-hash on an interface with no addresses' + atf_set require.user root +} + +no_addrs_source_hash_body() +{ + no_addrs_common "source-hash" +} + +no_addrs_source_hash_cleanup() +{ + pft_cleanup +} + +atf_test_case "no_addrs_random" "cleanup" +no_addrs_random_head() +{ + atf_set descr 'Test random on an interface with no addresses' + atf_set require.user root +} + +no_addrs_random_body() +{ + no_addrs_common "random" +} + +no_addrs_random_cleanup() +{ + pft_cleanup +} + +nat_pass_head() +{ + atf_set descr 'IPv4 NAT on pass rule' + atf_set require.user root + atf_set require.progs scapy +} + +nat_pass_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + pft_set_rules router \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp keep state" \ + "pass out on ${epair_server}a inet proto tcp nat-to ${epair_server}a keep state" + + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + jexec router pfctl -qvvsr + jexec router pfctl -qvvss + jexec router ifconfig + jexec router netstat -rn +} + +nat_pass_cleanup() +{ + pft_cleanup +} + +nat_match_head() +{ + atf_set descr 'IPv4 NAT on match rule' + atf_set require.user root + atf_set require.progs scapy +} + +nat_match_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + # NAT is applied during ruleset evaluation: + # rules after "match" match on NAT-ed address + pft_set_rules router \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp keep state" \ + "match out on ${epair_server}a inet proto tcp nat-to ${epair_server}a" \ + "pass out on ${epair_server}a inet proto tcp from ${epair_server}a keep state" + + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + jexec router pfctl -qvvsr + jexec router pfctl -qvvss + jexec router ifconfig + jexec router netstat -rn +} + +nat_match_cleanup() +{ + pft_cleanup +} + +map_e_common() +{ + NC_TRY_COUNT=12 + + pft_init + + epair_map_e=$(vnet_mkepair) + epair_echo=$(vnet_mkepair) + + vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a + vnet_mkjail echo ${epair_echo}b + + ifconfig ${epair_map_e}a 192.0.2.2/24 up + route add -net 198.51.100.0/24 192.0.2.1 + + jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up + jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up + jexec map_e sysctl net.inet.ip.forwarding=1 + + jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up + jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf + + # Enable pf! + jexec map_e pfctl -e +} + +atf_test_case "map_e_compat" "cleanup" +map_e_compat_head() +{ + atf_set descr 'map-e-portset test' + atf_set require.user root +} + +map_e_compat_body() +{ + map_e_common + + pft_set_rules map_e \ + "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342" + + # Only allow specified ports. + jexec echo pfctl -e + pft_set_rules echo "block return all" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ + "set skip on lo" + + i=0 + while [ ${i} -lt ${NC_TRY_COUNT} ] + do + echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 + if [ $? -ne 0 ]; then + atf_fail "nc failed (${i})" + fi + i=$((${i}+1)) + done +} + +map_e_compat_cleanup() +{ + pft_cleanup +} + + +atf_test_case "map_e_pass" "cleanup" +map_e_pass_head() +{ + atf_set descr 'map-e-portset test' + atf_set require.user root +} + +map_e_pass_body() +{ + map_e_common + + pft_set_rules map_e \ + "pass out on ${epair_echo}a inet from 192.0.2.0/24 to any nat-to (${epair_echo}a) map-e-portset 2/12/0x342 keep state" + + jexec map_e pfctl -qvvsr + + # Only allow specified ports. + jexec echo pfctl -e + pft_set_rules echo "block return all" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \ + "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \ + "set skip on lo" + + i=0 + while [ ${i} -lt ${NC_TRY_COUNT} ] + do + echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7 + if [ $? -ne 0 ]; then + atf_fail "nc failed (${i})" + fi + i=$((${i}+1)) + done +} + +map_e_pass_cleanup() +{ + pft_cleanup +} + +binat_compat_head() +{ + atf_set descr 'IPv4 BINAT with nat ruleset' + atf_set require.user root + atf_set require.progs scapy +} + +binat_compat_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + pft_set_rules router \ + "set state-policy if-bound" \ + "set ruleset-optimization none" \ + "binat on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag -> ${epair_server}a" \ + "block" \ + "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \ + "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state" + + # Test the outbound NAT part of BINAT. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 1" \ + "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 2" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Test the inbound RDR part of BINAT. + # The "tester" becomes "server" and vice versa. + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + inetd -p ${PWD}/inetd_tester.pid $inetd_conf + + atf_check -s exit:0 \ + jexec server ${common_dir}/pft_ping.py \ + --ping-type=tcp3way --send-sport=4202 \ + --sendif ${epair_server}b \ + --to ${net_server_host_router} \ + --replyif ${epair_server}b + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 3" \ + "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 4" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +binat_compat_cleanup() +{ + pft_cleanup + kill $(cat ${PWD}/inetd_tester.pid) +} + +binat_match_head() +{ + atf_set descr 'IPv4 BINAT with nat ruleset' + atf_set require.user root + atf_set require.progs scapy +} + +binat_match_body() +{ + setup_router_server_ipv4 + # Delete the route back to make sure that the traffic has been NAT-ed + jexec server route del -net ${net_tester} ${net_server_host_router} + + # The "binat-to" rule expands to 2 rules so the ""pass" rules start at 3! + pft_set_rules router \ + "set state-policy if-bound" \ + "set ruleset-optimization none" \ + "block" \ + "match on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag binat-to ${epair_server}a" \ + "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \ + "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \ + "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state" + + # Test the outbound NAT part of BINAT. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 3" \ + "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 4" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Test the inbound RDR part of BINAT. + # The "tester" becomes "server" and vice versa. + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + inetd -p ${PWD}/inetd_tester.pid $inetd_conf + + atf_check -s exit:0 \ + jexec server ${common_dir}/pft_ping.py \ + --ping-type=tcp3way --send-sport=4202 \ + --sendif ${epair_server}b \ + --to ${net_server_host_router} \ + --replyif ${epair_server}b + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 5" \ + "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 6" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +binat_match_cleanup() +{ + pft_cleanup + kill $(cat ${PWD}/inetd_tester.pid) +} + atf_init_test_cases() { atf_add_test_case "exhaust" atf_add_test_case "nested_anchor" + atf_add_test_case "endpoint_independent_compat" + atf_add_test_case "endpoint_independent_pass" + atf_add_test_case "nat6_nolinklocal" + atf_add_test_case "empty_table_source_hash" + atf_add_test_case "no_addrs_source_hash" + atf_add_test_case "empty_table_random" + atf_add_test_case "no_addrs_random" + atf_add_test_case "map_e_compat" + atf_add_test_case "map_e_pass" + atf_add_test_case "nat_pass" + atf_add_test_case "nat_match" + atf_add_test_case "binat_compat" + atf_add_test_case "binat_match" } diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py new file mode 100644 index 000000000000..adae2489ce5e --- /dev/null +++ b/tests/sys/netpfil/pf/nat64.py @@ -0,0 +1,274 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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 pytest +import selectors +import socket +import sys +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestNAT64(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2", "if3"]}, + "vnet4": {"ifaces": ["if3"]}, + "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]}, + "if2": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]}, + "if3": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]} + } + + def vnet4_handler(self, vnet): + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + + def vnet3_handler(self, vnet): + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.ttl=62") + ToolsHelper.print_output("/sbin/sysctl net.inet.udp.checksum=0") + + sel = selectors.DefaultSelector() + t = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + t.bind(("0.0.0.0", 1234)) + t.setblocking(False) + t.listen() + sel.register(t, selectors.EVENT_READ, data=None) + + u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + u.bind(("0.0.0.0", 4444)) + u.setblocking(False) + sel.register(u, selectors.EVENT_READ, data="UDP") + + while True: + events = sel.select(timeout=20) + for key, mask in events: + sock = key.fileobj + if key.data is None: + conn, addr = sock.accept() + print(f"Accepted connection from {addr}") + data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"") + events = selectors.EVENT_READ | selectors.EVENT_WRITE + sel.register(conn, events, data=data) + elif key.data == "UDP": + recv_data, addr = sock.recvfrom(1024) + print(f"Received UDP {recv_data} from {addr}") + sock.sendto(b"foo", addr) + else: + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + print(f"Received TCP {recv_data}") + sock.send(b"foo") + else: + print("Unknown event?") + t.close() + u.close() + return + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + + ToolsHelper.print_output("/sbin/route add default 192.0.2.2") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass inet6 proto icmp6", + "pass in on %s inet6 af-to inet from 192.0.2.1" % ifname]) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_tcp_rst(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + # Send a SYN + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.TCP(dport=1222, flags="S") + + # Get a reply + reply = sp.sr1(packet) + + # We expect to get a RST here. + tcp = reply.getlayer(sp.TCP) + assert tcp + assert "R" in tcp.flags + + # Now try to SYN to an open port + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.TCP(dport=1234, flags="S") + reply = sp.sr1(packet) + + tcp = reply.getlayer(sp.TCP) + assert tcp + + # We don't get RST + assert "R" not in tcp.flags + + # We do get SYN|ACK + assert "S" in tcp.flags + assert "A" in tcp.flags + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp_port_closed(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.UDP(dport=1222) / sp.Raw("bar") + reply = sp.sr1(packet, timeout=3) + print(reply.show()) + + # We expect an ICMPv6 error, not a UDP reply + assert not reply.getlayer(sp.UDP) + icmp = reply.getlayer(sp.ICMPv6DestUnreach) + assert icmp + assert icmp.type == 1 + assert icmp.code == 4 + udp = reply.getlayer(sp.UDPerror) + assert udp + assert udp.dport == 1222 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_address_unreachable(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::203.0.113.2") \ + / sp.UDP(dport=1222) / sp.Raw("bar") + reply = sp.sr1(packet, timeout=3) + print(reply.show()) + + # We expect an ICMPv6 error, not a UDP reply + assert not reply.getlayer(sp.UDP) + icmp = reply.getlayer(sp.ICMPv6DestUnreach) + assert icmp + assert icmp.type == 1 + assert icmp.code == 0 + udp = reply.getlayer(sp.UDPerror) + assert udp + assert udp.dport == 1222 + + # Check the hop limit + ip6 = reply.getlayer(sp.IPv6) + assert ip6.hlim == 61 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp_checksum(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + # Send an outbound UDP packet to establish state + packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \ + / sp.UDP(sport=3333, dport=4444) / sp.Raw("foo") + + # Get a reply + # We'll send the reply without UDP checksum on the IPv4 side + # but that's not valid for IPv6, so expect pf to update the checksum. + reply = sp.sr1(packet, timeout=5) + + udp = reply.getlayer(sp.UDP) + assert udp + assert udp.chksum != 0 + + def common_test_source_addr(self, packet): + vnet = self.vnet_map["vnet1"] + sendif = vnet.iface_alias_map["if1"].name + + import scapy.all as sp + + print("Outbound:\n") + packet.show() + + s = DelayedSend(packet) + + # We expect an ICMPv6 error here, where we'll verify the source address of + # the outer packet + packets = sp.sniff(iface=sendif, timeout=5) + + for reply in packets: + print("Reply:\n") + reply.show() + icmp = reply.getlayer(sp.ICMPv6TimeExceeded) + if not icmp: + continue + + ip = reply.getlayer(sp.IPv6) + assert icmp + assert ip.src == "64:ff9b::c000:202" + return reply + + # If we don't find the packet we expect to see + assert False + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_tcp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.TCP(sport=1111, dport=2222, flags="S") + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_udp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.UDP(sport=1111, dport=2222) / sp.Raw("foo") + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_sctp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.SCTP(sport=1111, dport=2222) \ + / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500) + self.common_test_source_addr(packet) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_source_addr_icmp(self): + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + import scapy.all as sp + + packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \ + / sp.ICMPv6EchoRequest() / sp.Raw("foo") + reply = self.common_test_source_addr(packet) + icmp = reply.getlayer(sp.ICMPv6EchoRequest) + assert icmp diff --git a/tests/sys/netpfil/pf/nat64.sh b/tests/sys/netpfil/pf/nat64.sh new file mode 100644 index 000000000000..0bba1470c4c5 --- /dev/null +++ b/tests/sys/netpfil/pf/nat64.sh @@ -0,0 +1,1056 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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. + +. $(atf_get_srcdir)/utils.subr + +nat64_setup_base() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e +} + +nat64_setup_in() +{ + nat64_setup_base + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" +} + +nat64_setup_out() +{ + nat64_setup_base + jexec rtr sysctl net.inet6.ip6.forwarding=1 + # AF translation happens post-routing, traffic must be directed + # towards the outbound interface using routes for the original AF. + # jexec rtr ifconfig ${epair_link}a inet6 2001:db8:2::1/64 up no_dad + jexec rtr route add -inet6 64:ff9b::/96 -iface ${epair_link}a; + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in quick on ${epair}b from any to 64:ff9b::/96" \ + "pass out quick on ${epair_link}a from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" \ + "block" +} + +atf_test_case "icmp_echo_in" "cleanup" +icmp_echo_in_head() +{ + atf_set descr 'Basic NAT64 ICMP echo test on inbound interface' + atf_set require.user root +} + +icmp_echo_in_body() +{ + nat64_setup_in + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +icmp_echo_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "icmp_echo_out" "cleanup" +icmp_echo_out_head() +{ + atf_set descr 'Basic NAT64 ICMP echo test on outbound interface' + atf_set require.user root +} + +icmp_echo_out_body() +{ + nat64_setup_out + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +icmp_echo_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "fragmentation_in" "cleanup" +fragmentation_in_head() +{ + atf_set descr 'Test fragmented packets on inbound interface' + atf_set require.user root +} + +fragmentation_in_body() +{ + nat64_setup_in + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -s 1280 64:ff9b::192.0.2.2 + + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 2000 64:ff9b::192.0.2.2 + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2 +} + +fragmentation_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "fragmentation_out" "cleanup" +fragmentation_out_head() +{ + atf_set descr 'Test fragmented packets on outbound interface' + atf_set require.user root +} + +fragmentation_out_body() +{ + nat64_setup_out + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -s 1280 64:ff9b::192.0.2.2 + + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 2000 64:ff9b::192.0.2.2 + atf_check -s exit:0 \ + -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \ + ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2 +} + +fragmentation_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_in" "cleanup" +tcp_in_head() +{ + atf_set descr 'TCP NAT64 test on inbound interface' + atf_set require.user root +} + +tcp_in_body() +{ + nat64_setup_in + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to TCP server" + fi +} + +tcp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_out" "cleanup" +tcp_out_head() +{ + atf_set descr 'TCP NAT64 test on outbound interface' + atf_set require.user root +} + +tcp_out_body() +{ + nat64_setup_out + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to TCP server" + fi +} + +tcp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "udp_in" "cleanup" +udp_in_head() +{ + atf_set descr 'UDP NAT64 test on inbound interface' + atf_set require.user root +} + +udp_in_body() +{ + nat64_setup_in + + echo "foo" | jexec dst nc -u -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to UDP server" + fi +} + +udp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "udp_out" "cleanup" +udp_out_head() +{ + atf_set descr 'UDP NAT64 test on outbound interface' + atf_set require.user root +} + +udp_out_body() +{ + nat64_setup_out + + echo "foo" | jexec dst nc -u -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to UDP server" + fi +} + +udp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "sctp_in" "cleanup" +sctp_in_head() +{ + atf_set descr 'SCTP NAT64 test on inbound interface' + atf_set require.user root +} + +sctp_in_body() +{ + nat64_setup_in + if ! kldstat -q -m sctp; then + atf_skip "This test requires SCTP" + fi + + echo "foo" | jexec dst nc --sctp -N -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to SCTP server" + fi +} + +sctp_in_cleanup() +{ + pft_cleanup +} + +atf_test_case "sctp_out" "cleanup" +sctp_out_head() +{ + atf_set descr 'SCTP NAT64 test on outbound interface' + atf_set require.user root +} + +sctp_out_body() +{ + nat64_setup_out + if ! kldstat -q -m sctp; then + atf_skip "This test requires SCTP" + fi + + echo "foo" | jexec dst nc --sctp -N -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) + if [ "${rcv}" != "foo" ]; + then + echo "rcv=${rcv}" + atf_fail "Failed to connect to SCTP server" + fi +} + +sctp_out_cleanup() +{ + pft_cleanup +} + +atf_test_case "tos" "cleanup" +tos_head() +{ + atf_set descr 'ToS translation test' + atf_set require.user root +} + +tos_body() +{ + nat64_setup_in + + # Ensure we can distinguish ToS on the destination + jexec dst pfctl -e + pft_set_rules dst \ + "pass" \ + "block in inet tos 8" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -z 4 64:ff9b::192.0.2.2 + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -z 8 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -z 16 64:ff9b::192.0.2.2 + + jexec dst pfctl -sr -vv +} + +tos_cleanup() +{ + pft_cleanup +} + +atf_test_case "no_v4" "cleanup" +no_v4_head() +{ + atf_set descr 'Test error handling when there is no IPv4 address to translate to' + atf_set require.user root +} + +no_v4_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + atf_check -s exit:2 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +no_v4_cleanup() +{ + pft_cleanup +} + +atf_test_case "range" "cleanup" +range_head() +{ + atf_set descr 'Test using an address range for the IPv4 side' + atf_set require.user root +} + +range_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up + jexec dst route add default 192.0.2.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.3 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.2/31 round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.2.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss +} + +range_cleanup() +{ + pft_cleanup +} + +atf_test_case "pool" "cleanup" +pool_head() +{ + atf_set descr 'Use a pool of IPv4 addresses' + atf_set require.user root +} + +pool_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from { 192.0.2.1, 192.0.2.3, 192.0.2.4 } round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.1.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.4.*" \ + jexec dst pfctl -ss +} + +pool_cleanup() +{ + pft_cleanup +} + + +atf_test_case "table" +table_head() +{ + atf_set descr 'Check table restrictions' + atf_set require.user root +} + +table_body() +{ + pft_init + + # Round-robin and random are allowed + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> round-robin" | \ + atf_check -s exit:0 \ + pfctl -f - + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> random" | \ + atf_check -s exit:0 \ + pfctl -f - + + # bitmask is not + echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> bitmask" | \ + atf_check -s exit:1 \ + -e match:"tables are not supported by pool type" \ + pfctl -f - +} + +table_cleanup() +{ + pft_cleanup +} + +atf_test_case "table_range" "cleanup" +table_range_head() +{ + atf_set descr 'Test using an address range within a table for the IPv4 side' + atf_set require.user root +} + +table_range_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up + jexec dst route add default 192.0.2.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.2 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <wanaddrs> { 192.0.2.2/31 }" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> round-robin" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.254 + + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.2.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss +} + +table_range_cleanup() +{ + pft_cleanup +} + +table_common_body() +{ + pool_type=$1 + + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <wanaddrs> { 192.0.2.1, 192.0.2.3, 192.0.2.4 }" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> ${pool_type}" + + # Use pf to count sources + jexec dst pfctl -e + pft_set_rules dst \ + "pass" + + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # XXX We can't reasonably check pool type random because it's random. It may end + # up choosing the same source IP for all three connections. + if [ "${pool_type}" == "round-robin" ]; + then + # Verify on dst that we saw different source addresses + atf_check -s exit:0 -o match:".*192.0.2.1.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.3.*" \ + jexec dst pfctl -ss + atf_check -s exit:0 -o match:".*192.0.2.4.*" \ + jexec dst pfctl -ss + fi +} + +atf_test_case "table_round_robin" "cleanup" +table_round_robin_head() +{ + atf_set descr 'Use a table of IPv4 addresses in round-robin mode' + atf_set require.user root +} + +table_round_robin_body() +{ + table_common_body round-robin +} + +table_round_robin_cleanup() +{ + pft_cleanup +} + +atf_test_case "table_random" "cleanup" +table_random_head() +{ + atf_set descr 'Use a table of IPv4 addresses in random mode' + atf_set require.user root +} + +table_random_body() +{ + table_common_body random +} + +table_random_cleanup() +{ + pft_cleanup +} + +atf_test_case "dummynet" "cleanup" +dummynet_head() +{ + atf_set descr 'Test dummynet on af-to rules' + atf_set require.user root +} + +dummynet_body() +{ + pft_init + dummynet_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + jexec rtr dnctl pipe 1 config delay 600 + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b inet6 from any to 64:ff9b::/96 dnpipe 1 af-to inet from (${epair_link}a)" + + # The ping request will pass, but take 1.2 seconds (.6 in, .6 out) + # So this works: + atf_check -s exit:0 -o ignore \ + ping6 -c 1 -t 2 64:ff9b::192.0.2.2 + + # But this times out: + atf_check -s exit:2 -o ignore \ + ping6 -c 1 -t 1 64:ff9b::192.0.2.2 +} + +dummynet_cleanup() +{ + pft_cleanup +} + +atf_test_case "gateway6" "cleanup" +gateway6_head() +{ + atf_set descr 'NAT64 with a routing hop on the v6 side' + atf_set require.user root +} + +gateway6_body() +{ + pft_init + + epair_lan_link=$(vnet_mkepair) + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8:1::2/64 up no_dad + route -6 add default 2001:db8:1::1 + + vnet_mkjail lan_rtr ${epair}b ${epair_lan_link}a + jexec lan_rtr ifconfig ${epair}b inet6 2001:db8:1::1/64 up no_dad + jexec lan_rtr ifconfig ${epair_lan_link}a inet6 2001:db8::2/64 up no_dad + jexec lan_rtr route -6 add default 2001:db8::1 + jexec lan_rtr sysctl net.inet6.ip6.forwarding=1 + + vnet_mkjail rtr ${epair_lan_link}b ${epair_link}a + jexec rtr ifconfig ${epair_lan_link}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + jexec rtr route -6 add default 2001:db8::2 + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8:1::1 + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec dst ping -c 1 192.0.2.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair_lan_link}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + # One ping + atf_check -s exit:0 -o ignore \ + ping6 -c 1 64:ff9b::192.0.2.2 + + # Make sure packets make it even when state is established + atf_check -s exit:0 \ + -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \ + ping6 -c 5 64:ff9b::192.0.2.2 +} + +gateway6_cleanup() +{ + pft_cleanup +} + +atf_test_case "route_to" "cleanup" +route_to_head() +{ + atf_set descr 'Test route-to on af-to rules' + atf_set require.user root +} + +route_to_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair_null=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a ${epair_null}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_null}a 192.0.2.3/24 up + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b route-to (${epair_link}a 192.0.2.2) inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +route_to_cleanup() +{ + pft_cleanup +} + +atf_test_case "reply_to" "cleanup" +reply_to_head() +{ + atf_set descr 'Test reply-to on af-to rules' + atf_set require.user root +} + +reply_to_body() +{ + pft_init + + epair_link=$(vnet_mkepair) + epair=$(vnet_mkepair) + + ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair}b ${epair_link}a + jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up + + vnet_mkjail dst ${epair_link}b + jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up + jexec dst route add default 192.0.2.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair}b reply-to (${epair}b 2001:db8::2) inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.1" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 +} + +reply_to_cleanup() +{ + pft_cleanup +} + +atf_test_case "v6_gateway" "cleanup" +v6_gateway_head() +{ + atf_set descr 'nat64 when the IPv4 gateway is given by an IPv6 address' + atf_set require.user root +} + +v6_gateway_body() +{ + pft_init + + epair_wan_two=$(vnet_mkepair) + epair_wan_one=$(vnet_mkepair) + epair_lan=$(vnet_mkepair) + + ifconfig ${epair_lan}a inet6 2001:db8::2/64 up no_dad + route -6 add default 2001:db8::1 + + vnet_mkjail rtr ${epair_lan}b ${epair_wan_one}a + jexec rtr ifconfig ${epair_lan}b inet6 2001:db8::1/64 up no_dad + jexec rtr ifconfig ${epair_wan_one}a 192.0.2.1/24 up + jexec rtr ifconfig ${epair_wan_one}a inet6 -ifdisabled + jexec rtr route add default -inet6 fe80::1%${epair_wan_one}a + #jexec rtr route add default 192.0.2.2 + + vnet_mkjail wan_one ${epair_wan_one}b ${epair_wan_two}a + jexec wan_one ifconfig ${epair_wan_one}b 192.0.2.2/24 up + jexec wan_one ifconfig ${epair_wan_one}b inet6 fe80::1/64 + jexec wan_one ifconfig ${epair_wan_two}a 198.51.100.2/24 up + jexec wan_one route add default 192.0.2.1 + jexec wan_one sysctl net.inet.ip.forwarding=1 + + vnet_mkjail wan_two ${epair_wan_two}b + jexec wan_two ifconfig ${epair_wan_two}b 198.51.100.1/24 up + jexec wan_two route add default 198.51.100.2 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + jexec rtr ping -c 1 198.51.100.1 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "pass in on ${epair_lan}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_wan_one}a)" + + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping6 -c 3 64:ff9b::198.51.100.1 +} + +v6_gateway_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "icmp_echo_in" + atf_add_test_case "icmp_echo_out" + atf_add_test_case "fragmentation_in" + atf_add_test_case "fragmentation_out" + atf_add_test_case "tcp_in" + atf_add_test_case "tcp_out" + atf_add_test_case "udp_in" + atf_add_test_case "udp_out" + atf_add_test_case "sctp_in" + atf_add_test_case "sctp_out" + atf_add_test_case "tos" + atf_add_test_case "no_v4" + atf_add_test_case "range" + atf_add_test_case "pool" + atf_add_test_case "table" + atf_add_test_case "table_range" + atf_add_test_case "table_round_robin" + atf_add_test_case "table_random" + atf_add_test_case "dummynet" + atf_add_test_case "gateway6" + atf_add_test_case "route_to" + atf_add_test_case "reply_to" + atf_add_test_case "v6_gateway" +} diff --git a/tests/sys/netpfil/pf/nat66.py b/tests/sys/netpfil/pf/nat66.py index 3a037ac710fc..16b4ef3dd02b 100644 --- a/tests/sys/netpfil/pf/nat66.py +++ b/tests/sys/netpfil/pf/nat66.py @@ -29,23 +29,10 @@ import ipaddress import pytest import re import socket -import threading -import time +from utils import DelayedSend from atf_python.sys.net.tools import ToolsHelper from atf_python.sys.net.vnet import VnetTestTemplate -class DelayedSend(threading.Thread): - def __init__(self, packet): - threading.Thread.__init__(self) - self._packet = packet - - self.start() - - def run(self): - import scapy.all as sp - time.sleep(1) - sp.send(self._packet) - class TestNAT66(VnetTestTemplate): REQUIRED_MODULES = [ "pf" ] TOPOLOGY = { @@ -140,6 +127,7 @@ class TestNAT66(VnetTestTemplate): assert found @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_npt_icmp(self): cl_vnet = self.vnet_map["vnet1"] ifname = cl_vnet.iface_alias_map["if1"].name @@ -168,6 +156,7 @@ class TestNAT66(VnetTestTemplate): self.check_icmp_too_big(sp, 12000, 5000) @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) def test_npt_route_to_icmp(self): cl_vnet = self.vnet_map["vnet1"] ifname = cl_vnet.iface_alias_map["if1"].name diff --git a/tests/sys/netpfil/pf/pass_block.sh b/tests/sys/netpfil/pf/pass_block.sh index faf5c2de4de2..e955068d014b 100644 --- a/tests/sys/netpfil/pf/pass_block.sh +++ b/tests/sys/netpfil/pf/pass_block.sh @@ -232,7 +232,7 @@ urpf_head() { atf_set descr "Test unicast reverse path forwarding check" atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } urpf_body() @@ -292,6 +292,165 @@ urpf_cleanup() pft_cleanup } +atf_test_case "received_on" "cleanup" +received_on_head() +{ + atf_set descr 'Test received-on filtering' + atf_set require.user root +} + +received_on_body() +{ + pft_init + + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + epair_route=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair_one}b ${epair_two}b ${epair_route}a + vnet_mkjail srv ${epair_route}b + + ifconfig ${epair_one}a 192.0.2.2/24 up + ifconfig ${epair_two}a 198.51.100.2/24 up + route add 203.0.113.2 192.0.2.1 + route add 203.0.113.3 198.51.100.1 + + jexec alcatraz ifconfig ${epair_one}b 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair_two}b 198.51.100.1/24 up + jexec alcatraz ifconfig ${epair_route}a 203.0.113.1/24 up + jexec alcatraz sysctl net.inet.ip.forwarding=1 + + jexec srv ifconfig ${epair_route}b 203.0.113.2/24 up + jexec srv ifconfig ${epair_route}b inet alias 203.0.113.3/24 up + jexec srv route add default 203.0.113.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.3 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block in" \ + "pass received-on ${epair_one}b" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 + + # And ensure we can check the received-on interface after routing + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:2 -o ignore \ + ping -c 1 203.0.113.3 + + # Now try this with a group instead + jexec alcatraz ifconfig ${epair_one}b group test + pft_set_rules alcatraz \ + "block in" \ + "pass received-on test" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 + + # And ensure we can check the received-on interface after routing + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + atf_check -s exit:2 -o ignore \ + ping -c 1 203.0.113.3 + + # Test '! received-on' + pft_set_rules alcatraz \ + "pass in" \ + "block ! received-on ${epair_one}b" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + atf_check -s exit:2 -o ignore \ + ping -c 1 198.51.100.1 +} + +received_on_cleanup() +{ + pft_cleanup +} + +atf_test_case "optimize_any" "cleanup" +optimize_any_head() +{ + atf_set descr 'Test known optimizer bug' + atf_set require.user root +} + +optimize_any_body() +{ + pft_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + + ifconfig ${epair}b 192.0.2.2/24 up + + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass in inet from { any, 192.0.2.3 }" + + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 +} + +optimize_any_cleanup() +{ + pft_cleanup +} + +atf_test_case "any_if" "cleanup" +any_if_head() +{ + atf_set descr 'Test the any interface keyword' + atf_set require.user root +} + +any_if_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Sanity check + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "block" \ + "pass in on any" + + atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1 +} + +any_if_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "enable_disable" @@ -300,4 +459,7 @@ atf_init_test_cases() atf_add_test_case "noalias" atf_add_test_case "nested_inline" atf_add_test_case "urpf" + atf_add_test_case "received_on" + atf_add_test_case "optimize_any" + atf_add_test_case "any_if" } diff --git a/tests/sys/netpfil/pf/pflog.sh b/tests/sys/netpfil/pf/pflog.sh index 75b7c5c217bb..a34ec893a75c 100644 --- a/tests/sys/netpfil/pf/pflog.sh +++ b/tests/sys/netpfil/pf/pflog.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2023 Rubicon Communications, LLC (Netgate) +# Copyright (c) 2024 Deciso B.V. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -33,7 +34,7 @@ malformed_head() { atf_set descr 'Test that we do not log malformed packets as passing' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } malformed_body() @@ -79,7 +80,327 @@ malformed_cleanup() pft_cleanup } +atf_test_case "matches" "cleanup" +matches_head() +{ + atf_set descr 'Test the pflog matches keyword' + atf_set require.user root +} + +matches_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz \ + "match log(matches) inet proto icmp" \ + "match log(matches) inet from 192.0.2.2" \ + "pass" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log" + cat ${PWD}/pflog.txt + + atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt +} + +matches_cleanup() +{ + pft_cleanup +} + +atf_test_case "matches_logif" "cleanup" +matches_logif_head() +{ + atf_set descr 'Test log(matches, to pflogX)' + atf_set require.user root +} + +matches_logif_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + jexec alcatraz ifconfig pflog1 create + jexec alcatraz ifconfig pflog1 up + pft_set_rules alcatraz \ + "match log(matches, to pflog1) inet proto icmp" \ + "match log inet from 192.0.2.2" \ + "pass log(to pflog0)" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog1 >> ${PWD}/pflog1.txt & + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog0.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log 0" + cat ${PWD}/pflog0.txt + echo "Log 1" + cat ${PWD}/pflog1.txt + + atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog1.txt + atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog1.txt +} + +matches_logif_cleanup() +{ + pft_cleanup +} + +atf_test_case "state_max" "cleanup" +state_max_head() +{ + atf_set descr 'Ensure that drops due to state limits are logged' + atf_set require.user root +} + +state_max_body() +{ + pflog_init + + epair=$(vnet_mkepair) + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + ifconfig ${epair}b 192.0.2.2/24 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "pass log inet keep state (max 1)" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + echo "Rules" + jexec alcatraz pfctl -sr -vv + echo "States" + jexec alcatraz pfctl -ss -vv + echo "Log" + cat ${PWD}/pflog.txt + + # First ping passes. + atf_check -o match:".*rule 0/0\(match\): pass in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + + # Second ping is blocked due to the state limit. + atf_check -o match:".*rule 0/12\(state-limit\): block in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + cat pflog.txt + + # At most three lines should be written: one for the first ping, and + # two for the second: one for the initial pass through the ruleset, and + # then a drop because of the state limit. Ideally only the drop would + # be logged; if this is fixed, the count will be 2 instead of 3. + atf_check -o match:3 grep -c . pflog.txt + + # If the rule doesn't specify logging, we shouldn't log drops + # due to state limits. + pft_set_rules alcatraz "pass inet keep state (max 1)" + + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -s exit:2 -o ignore \ + ping -c 1 192.0.2.1 + + atf_check -o match:3 grep -c . pflog.txt +} + +state_max_cleanup() +{ + pft_cleanup +} + +atf_test_case "unspecified_v4" "cleanup" +unspecified_v4_head() +{ + atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks' + atf_set require.user root +} + +unspecified_v4_body() +{ + pflog_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 inet 127.0.0.1 + jexec alcatraz route add default 127.0.0.1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "block log on lo0 to 0.0.0.0" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s not-exit:0 -o ignore -e ignore \ + jexec alcatraz ping -S 127.0.0.1 -c 1 0.0.0.0 + + atf_check -o match:".*: block out on lo0: 127.0.0.1 > 0.0.0.0: ICMP echo request,.*" \ + cat pflog.txt +} + +unspecified_v4_cleanup() +{ + pft_cleanup +} + +atf_test_case "unspecified_v6" "cleanup" +unspecified_v6_head() +{ + atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks' + atf_set require.user root +} + +unspecified_v6_body() +{ + pflog_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 up + jexec alcatraz route -6 add ::0 ::1 + + jexec alcatraz pfctl -e + jexec alcatraz ifconfig pflog0 up + pft_set_rules alcatraz "block log on lo0 to ::0" + + jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt & + sleep 1 # Wait for tcpdump to start + + atf_check -s not-exit:0 -o ignore -e ignore \ + jexec alcatraz ping -6 -S ::1 -c 1 ::0 + + cat pflog.txt + atf_check -o match:".*: block out on lo0: ::1 > ::: ICMP6, echo request,.*" \ + cat pflog.txt +} + +unspecified_v6_cleanup() +{ + pft_cleanup +} + +atf_test_case "rdr_action" "cleanup" +rdr_head() +{ + atf_set descr 'Ensure that NAT rule actions are logged correctly' + atf_set require.user root +} + +rdr_action_body() +{ + pflog_init + + j="pflog:rdr_action" + epair_c=$(vnet_mkepair) + epair_srv=$(vnet_mkepair) + + vnet_mkjail ${j}srv ${epair_srv}a + vnet_mkjail ${j}gw ${epair_srv}b ${epair_c}a + vnet_mkjail ${j}c ${epair_c}b + + jexec ${j}srv ifconfig ${epair_srv}a 198.51.100.1/24 up + # No default route in srv jail, to ensure we're NAT-ing + jexec ${j}gw ifconfig ${epair_srv}b 198.51.100.2/24 up + jexec ${j}gw ifconfig ${epair_c}a 192.0.2.1/24 up + jexec ${j}gw sysctl net.inet.ip.forwarding=1 + jexec ${j}c ifconfig ${epair_c}b 192.0.2.2/24 up + jexec ${j}c route add default 192.0.2.1 + + jexec ${j}gw pfctl -e + jexec ${j}gw ifconfig pflog0 up + pft_set_rules ${j}gw \ + "rdr log on ${epair_srv}b proto tcp from 198.51.100.0/24 to any port 1234 -> 192.0.2.2 port 1234" \ + "block quick inet6" \ + "pass in log" + + jexec ${j}gw tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt & + sleep 1 # Wait for tcpdump to start + + # send a SYN to catch in the log + jexec ${j}srv nc -N -w 0 198.51.100.2 1234 + + echo "Log" + cat ${PWD}/pflog.txt + + # log line generated for rdr hit (pre-NAT) + atf_check -o match:".*.*rule 0/0\(match\): rdr in on ${epair_srv}b: 198.51.100.1.[0-9]* > 198.51.100.2.1234: Flags \[S\].*" \ + cat pflog.txt + + # log line generated for pass hit (post-NAT) + atf_check -o match:".*.*rule 1/0\(match\): pass in on ${epair_srv}b: 198.51.100.1.[0-9]* > 192.0.2.2.1234: Flags \[S\].*" \ + cat pflog.txt + + # only two log lines shall be written + atf_check -o match:2 grep -c . pflog.txt +} + +rdr_action_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "malformed" + atf_add_test_case "matches" + atf_add_test_case "matches_logif" + atf_add_test_case "state_max" + atf_add_test_case "unspecified_v4" + atf_add_test_case "unspecified_v6" + atf_add_test_case "rdr_action" } diff --git a/tests/sys/netpfil/pf/pflow.sh b/tests/sys/netpfil/pf/pflow.sh index f0552eb061da..1122096d2e31 100644 --- a/tests/sys/netpfil/pf/pflow.sh +++ b/tests/sys/netpfil/pf/pflow.sh @@ -85,7 +85,7 @@ state_defaults_head() { atf_set descr 'Test set state-defaults pflow' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } state_defaults_body() @@ -146,7 +146,7 @@ v6_head() { atf_set descr 'Test pflow over IPv6' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -188,7 +188,7 @@ nat_head() { atf_set descr 'Test pflow export for NAT44' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nat_body() @@ -239,7 +239,7 @@ rule_head() { atf_set descr 'Test per-rule pflow option' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } rule_body() diff --git a/tests/sys/netpfil/pf/pfsync.sh b/tests/sys/netpfil/pf/pfsync.sh index 87dfcf748d3c..7f545b43a066 100644 --- a/tests/sys/netpfil/pf/pfsync.sh +++ b/tests/sys/netpfil/pf/pfsync.sh @@ -126,7 +126,7 @@ defer_head() { atf_set descr 'Defer mode pfsync test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } defer_body() @@ -335,6 +335,7 @@ pbr_common_body() atf_skip "This test requires carp" fi pfsynct_init + vnet_init_bridge bridge0=$(vnet_mkbridge) bridge1=$(vnet_mkbridge) @@ -834,16 +835,11 @@ basic_ipv6_cleanup() pfsynct_cleanup } -atf_test_case "route_to" "cleanup" -route_to_head() +route_to_common_head() { - atf_set descr 'Test route-to with default rule' - atf_set require.user root - atf_set require.progs scapy -} + pfsync_version=$1 + shift -route_to_body() -{ pfsynct_init epair_sync=$(vnet_mkepair) @@ -865,40 +861,111 @@ route_to_body() jexec one ifconfig pfsync0 \ syncdev ${epair_sync}a \ maxupd 1 \ + version $pfsync_version \ up jexec two ifconfig ${epair_sync}b 192.0.2.2/24 up jexec two ifconfig ${epair_two}a 198.51.100.2/24 up jexec two ifconfig ${epair_out_two}a 203.0.113.2/24 up - #jexec two ifconfig ${epair_out_two}a name outif + jexec two ifconfig ${epair_out_two}a name outif jexec two sysctl net.inet.ip.forwarding=1 jexec two arp -s 203.0.113.254 00:01:02:03:04:05 jexec two ifconfig pfsync0 \ syncdev ${epair_sync}b \ maxupd 1 \ + version $pfsync_version \ up - # Enable pf! + ifconfig ${epair_one}b 198.51.100.254/24 up + ifconfig ${epair_two}b 198.51.100.253/24 up + route add -net 203.0.113.0/24 198.51.100.1 + ifconfig ${epair_two}b up + ifconfig ${epair_out_one}b up + ifconfig ${epair_out_two}b up +} + +route_to_common_tail() +{ + atf_check -s exit:0 env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_one}b \ + --fromaddr 198.51.100.254 \ + --to 203.0.113.254 \ + --recvif ${epair_out_one}b + + # Allow time for sync + sleep 2 + + states_one=$(mktemp) + states_two=$(mktemp) + jexec one pfctl -qvvss | normalize_pfctl_s > $states_one + jexec two pfctl -qvvss | normalize_pfctl_s > $states_two +} + +atf_test_case "route_to_1301_body" "cleanup" +route_to_1301_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_body() +{ + route_to_common_head 1301 + jexec one pfctl -e pft_set_rules one \ "set skip on ${epair_sync}a" \ "pass out route-to (outif 203.0.113.254)" + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to (outif 203.0.113.254)" + + route_to_common_tail - # Make sure we have different rulesets so the synced state is associated with - # V_pf_default_rule + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # With identical ruleset the routing information is recovered from the matching rule. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif' $states_two || + atf_fail "State missing on router two" + + true +} + +route_to_1301_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1301_bad_ruleset" "cleanup" +route_to_1301_bad_ruleset_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1 and incompatible ruleset' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_bad_ruleset_body() +{ + route_to_common_head 1301 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e pft_set_rules two \ + "set debug loud" \ "set skip on ${epair_sync}b" \ "pass out route-to (outif 203.0.113.254)" \ "pass out proto tcp" - ifconfig ${epair_one}b 198.51.100.254/24 up - ifconfig ${epair_two}b 198.51.100.253/24 up - route add -net 203.0.113.0/24 198.51.100.1 - ifconfig ${epair_two}b up - ifconfig ${epair_out_one}b up - ifconfig ${epair_out_two}b up - atf_check -s exit:0 env PYTHONPATH=${common_dir} \ ${common_dir}/pft_ping.py \ --sendif ${epair_one}b \ @@ -906,25 +973,151 @@ route_to_body() --to 203.0.113.254 \ --recvif ${epair_out_one}b - # Allow time for sync - ifconfig ${epair_one}b inet 198.51.100.254 -alias - route del -net 203.0.113.0/24 198.51.100.1 - route add -net 203.0.113.0/24 198.51.100.2 + route_to_common_tail - sleep 2 + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" - # Now try to trigger the state on the other pfsync member - env PYTHONPATH=${common_dir} \ + # Different ruleset on each router means the routing information recovery + # from rule is impossible. The state is not synced. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" + + true +} + +route_to_1301_bad_ruleset_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1301_bad_rpool" "cleanup" +route_to_1301_bad_rpool_head() +{ + atf_set descr 'Test route-to with pfsync version 13.1 and different interface' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1301_bad_rpool_body() +{ + route_to_common_head 1301 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }" + + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }" + + atf_check -s exit:0 env PYTHONPATH=${common_dir} \ ${common_dir}/pft_ping.py \ - --sendif ${epair_two}b \ + --sendif ${epair_one}b \ --fromaddr 198.51.100.254 \ --to 203.0.113.254 \ - --recvif ${epair_out_two}b + --recvif ${epair_out_one}b + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # The ruleset is identical but since the redirection pool contains multiple interfaces + # pfsync will not attempt to recover the routing information from the rule. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" + + true +} + +route_to_1301_bad_rpool_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1400_bad_ruleset" "cleanup" +route_to_1400_bad_ruleset_head() +{ + atf_set descr 'Test route-to with pfsync version 14.0' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1400_bad_ruleset_body() +{ + route_to_common_head 1400 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # Even with a different ruleset FreeBSD 14 syncs the state just fine. + # There's no recovery involved, the pfsync packet contains the routing information. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .* route-to: 203.0.113.254@outif' $states_two || + atf_fail "State missing on router two" + + true +} + +route_to_1400_bad_ruleset_cleanup() +{ + pfsynct_cleanup +} + +atf_test_case "route_to_1400_bad_ifname" "cleanup" +route_to_1400_bad_ifname_head() +{ + atf_set descr 'Test route-to with pfsync version 14.0' + atf_set require.user root + atf_set require.progs python3 scapy +} + +route_to_1400_bad_ifname_body() +{ + route_to_common_head 1400 + + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass out route-to (outif 203.0.113.254)" + + jexec two pfctl -e + jexec two ifconfig outif name outif_new + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass out route-to (outif_new 203.0.113.254)" + + route_to_common_tail + + # Sanity check + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one || + atf_fail "State missing on router one" + + # Since FreeBSD 14 never attempts recovery of missing routing information + # a state synced to a router with a different interface name is dropped. + grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two && + atf_fail "State present on router two" true } -route_to_cleanup() +route_to_1400_bad_ifname_cleanup() { pfsynct_cleanup } @@ -941,5 +1134,9 @@ atf_init_test_cases() atf_add_test_case "timeout" atf_add_test_case "basic_ipv6_unicast" atf_add_test_case "basic_ipv6" - atf_add_test_case "route_to" + atf_add_test_case "route_to_1301" + atf_add_test_case "route_to_1301_bad_ruleset" + atf_add_test_case "route_to_1301_bad_rpool" + atf_add_test_case "route_to_1400_bad_ruleset" + atf_add_test_case "route_to_1400_bad_ifname" } diff --git a/tests/sys/netpfil/pf/pft_read_ipfix.py b/tests/sys/netpfil/pf/pft_read_ipfix.py index 2c11bdfd130c..02ef2ca5ab06 100644 --- a/tests/sys/netpfil/pf/pft_read_ipfix.py +++ b/tests/sys/netpfil/pf/pft_read_ipfix.py @@ -60,7 +60,7 @@ def parse_ipfix(p): c = datafl.payload def receive(recvif, recvport): - pkts = sp.sniff(iface=recvif, timeout=65) + pkts = sp.sniff(iface=recvif, timeout=65, filter="udp port 2055") if len(pkts) == 0: print("No data") diff --git a/tests/sys/netpfil/pf/proxy.sh b/tests/sys/netpfil/pf/proxy.sh index 4a7ea00a0cd4..78ce25930c04 100644 --- a/tests/sys/netpfil/pf/proxy.sh +++ b/tests/sys/netpfil/pf/proxy.sh @@ -57,7 +57,7 @@ ftp_body() jexec srv route add default 198.51.100.1 # Start FTP server in srv - jexec srv twistd ftp -r `pwd` -p 21 + jexec srv twistd --logfile=/dev/null ftp -r `pwd` -p 21 # Sanity check atf_check -s exit:0 -o ignore ping -c 1 198.51.100.2 @@ -74,7 +74,7 @@ ftp_body() # Create a dummy file to download echo 'foo' > remote.txt - echo 'get remote.txt local.txt' | ftp -a 198.51.100.2 + echo -e 'epsv\nget remote.txt local.txt' | ftp -a 198.51.100.2 # Compare the downloaded file to the original if ! diff -q local.txt remote.txt; diff --git a/tests/sys/netpfil/pf/rdr-srcport.py b/tests/sys/netpfil/pf/rdr-srcport.py new file mode 100644 index 000000000000..633580582711 --- /dev/null +++ b/tests/sys/netpfil/pf/rdr-srcport.py @@ -0,0 +1,20 @@ +# +# A helper script which accepts TCP connections and writes the remote port +# number to the stream. +# + +import socket + +def main(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(('0.0.0.0', 8888)) + s.listen(5) + + while True: + cs, addr = s.accept() + cs.sendall(str(addr[1]).encode()) + cs.close() + +if __name__ == '__main__': + main() diff --git a/tests/sys/netpfil/pf/rdr.sh b/tests/sys/netpfil/pf/rdr.sh index 5e60b97c653b..4c08b4973891 100644 --- a/tests/sys/netpfil/pf/rdr.sh +++ b/tests/sys/netpfil/pf/rdr.sh @@ -27,14 +27,6 @@ . $(atf_get_srcdir)/utils.subr -atf_test_case "tcp_v6" "cleanup" -tcp_v6_head() -{ - atf_set descr 'TCP rdr with IPv6' - atf_set require.user root - atf_set require.progs scapy python3 -} - # # Test that rdr works for TCP with IPv6. # @@ -47,7 +39,7 @@ tcp_v6_head() # # Test for incorrect checksums after the rewrite by looking at a packet capture (see bug 210860) # -tcp_v6_body() +tcp_v6_setup() { pft_init @@ -83,16 +75,18 @@ tcp_v6_body() jexec ${j}c route add -inet6 2001:db8:a::0/64 2001:db8:b::1 jexec ${j}b pfctl -e +} - pft_set_rules ${j}b \ - "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000" +tcp_v6_common() +{ + pft_set_rules ${j}b "${1}" # Check that a can reach c over the router atf_check -s exit:0 -o ignore \ jexec ${j}a ping -6 -c 1 2001:db8:b::2 # capture packets on c so we can look for incorrect checksums - jexec ${j}c tcpdump --immediate-mode -w ${j}.pcap tcp and port 8000 & + jexec ${j}c tcpdump --immediate-mode -w ${PWD}/${j}.pcap tcp and port 8000 & tcpdumppid=$! # start a web server and give it a second to start @@ -112,16 +106,185 @@ tcp_v6_body() # Check for 'incorrect' in packet capture, this should tell us if # checksums are bad with rdr rules - count=$(jexec ${j}c tcpdump -vvvv -r ${j}.pcap | grep incorrect | wc -l) + count=$(jexec ${j}c tcpdump -vvvv -r ${PWD}/${j}.pcap | grep incorrect | wc -l) atf_check_equal " 0" "$count" } -tcp_v6_cleanup() +atf_test_case "tcp_v6_compat" "cleanup" +tcp_v6_compat_head() +{ + atf_set descr 'TCP rdr with IPv6 with NAT rules' + atf_set require.user root + atf_set require.progs python3 +} + +tcp_v6_compat_body() +{ + tcp_v6_setup # Sets ${epair_…} variables + tcp_v6_common \ + "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000" +} + +tcp_v6_compat_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_v6_pass" "cleanup" +tcp_v6_pass_head() +{ + atf_set descr 'TCP rdr with IPv6 with pass/match rules' + atf_set require.user root + atf_set require.progs python3 +} + +tcp_v6_pass_body() +{ + tcp_v6_setup # Sets ${epair_…} variables + tcp_v6_common \ + "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000" +} + +tcp_v6_pass_cleanup() +{ + pft_cleanup +} + +# +# Test that rdr works for multiple TCP with same srcip and srcport. +# +# Four jails, a, b, c, d, are used: +# - jail d runs a server on port 8888, +# - jail a makes connections to the server, routed through jails b and c, +# - jail b uses NAT to rewrite source addresses and ports to the same 2-tuple, +# avoiding the need to use SO_REUSEADDR in jail a, +# - jail c uses a redirect rule to map the destination address to the same +# address and port, resulting in a NAT state conflict. +# +# In this case, the rdr rule should also rewrite the source port (again) to +# resolve the state conflict. +# +srcport_setup() +{ + pft_init + + j="rdr:srcport" + epair1=$(vnet_mkepair) + epair2=$(vnet_mkepair) + epair3=$(vnet_mkepair) + + echo $epair_one + echo $epair_two + + vnet_mkjail ${j}a ${epair1}a + vnet_mkjail ${j}b ${epair1}b ${epair2}a + vnet_mkjail ${j}c ${epair2}b ${epair3}a + vnet_mkjail ${j}d ${epair3}b + + # configure addresses for a + jexec ${j}a ifconfig lo0 up + jexec ${j}a ifconfig ${epair1}a inet 198.51.100.50/24 up + jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.51/24 + jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.52/24 + + # configure addresses for b + jexec ${j}b ifconfig lo0 up + jexec ${j}b ifconfig ${epair1}b inet 198.51.100.1/24 up + jexec ${j}b ifconfig ${epair2}a inet 198.51.101.2/24 up + + # configure addresses for c + jexec ${j}c ifconfig lo0 up + jexec ${j}c ifconfig ${epair2}b inet 198.51.101.3/24 up + jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.4/24 + jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.5/24 + jexec ${j}c ifconfig ${epair3}a inet 203.0.113.1/24 up + + # configure addresses for d + jexec ${j}d ifconfig lo0 up + jexec ${j}d ifconfig ${epair3}b inet 203.0.113.50/24 up + + jexec ${j}b sysctl net.inet.ip.forwarding=1 + jexec ${j}c sysctl net.inet.ip.forwarding=1 + jexec ${j}b pfctl -e + jexec ${j}c pfctl -e +} + +srcport_common() +{ + pft_set_rules ${j}b \ + "set debug misc" \ + "${1}" + + pft_set_rules ${j}c \ + "set debug misc" \ + "${2}" + + jexec ${j}a route add default 198.51.100.1 + jexec ${j}c route add 198.51.100.0/24 198.51.101.2 + jexec ${j}d route add 198.51.101.0/24 203.0.113.1 + + jexec ${j}d python3 $(atf_get_srcdir)/rdr-srcport.py & + sleep 1 + + echo a | jexec ${j}a nc -w 3 -s 198.51.100.50 -p 1234 198.51.101.3 7777 > port1 + + jexec ${j}a nc -s 198.51.100.51 -p 1234 198.51.101.4 7777 > port2 & + jexec ${j}a nc -s 198.51.100.52 -p 1234 198.51.101.5 7777 > port3 & + sleep 1 + + atf_check -o inline:"1234" cat port1 + atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port2 + atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port3 +} + +atf_test_case "srcport_compat" "cleanup" +srcport_compat_head() +{ + atf_set descr 'TCP rdr srcport modulation with NAT rules' + atf_set require.user root + atf_set require.progs python3 + atf_set timeout 9999 +} + +srcport_compat_body() +{ + srcport_setup # Sets ${epair_…} variables + srcport_common \ + "nat on ${epair2}a inet from 198.51.100.0/24 to any -> ${epair2}a static-port" \ + "rdr on ${epair2}b proto tcp from any to ${epair2}b port 7777 -> 203.0.113.50 port 8888" +} + +srcport_compat_cleanup() +{ + pft_cleanup +} + +atf_test_case "srcport_pass" "cleanup" +srcport_pass_head() +{ + atf_set descr 'TCP rdr srcport modulation with pass/match rules' + atf_set require.user root + atf_set require.progs python3 + atf_set timeout 9999 +} + +srcport_pass_body() +{ + srcport_setup # Sets ${epair_…} variables + srcport_common \ + "pass out on ${epair2}a inet from 198.51.100.0/24 to any nat-to ${epair2}a static-port" \ + "pass in on ${epair2}b proto tcp from any to ${epair2}b port 7777 rdr-to 203.0.113.50 port 8888" +} + +srcport_pass_cleanup() { pft_cleanup } atf_init_test_cases() { - atf_add_test_case "tcp_v6" + atf_add_test_case "tcp_v6_compat" + atf_add_test_case "tcp_v6_pass" + atf_add_test_case "srcport_compat" + atf_add_test_case "srcport_pass" } diff --git a/tests/sys/netpfil/pf/return.py b/tests/sys/netpfil/pf/return.py new file mode 100644 index 000000000000..753012860764 --- /dev/null +++ b/tests/sys/netpfil/pf/return.py @@ -0,0 +1,153 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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 pytest +import subprocess +import re +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +def check(cmd): + ps = subprocess.Popen(cmd, shell=True) + ret = ps.wait() + if ret != 0: + raise Exception("Command %s returned %d" % (cmd, ret)) + +class TestReturn(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + if2name = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (ifname, ifname), + "block return", + "pass inet proto icmp icmp-type echoreq", + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + ToolsHelper.print_output("/sbin/pfctl -x loud") + + def vnet3_handler(self, vnet): + ToolsHelper.print_output("/sbin/route add default 198.51.100.1") + + def common_setup(self): + ToolsHelper.print_output("/sbin/route add default 192.0.2.1") + + # Sanity check + check("/sbin/ping -c 1 192.0.2.1") + check("/sbin/ping -c 1 198.51.100.1") + check("/sbin/ping -c 2 198.51.100.2") + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_tcp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send a TCP SYN, expect a RST + pkt = sp.IP(src="192.0.2.2", dst="198.51.100.2") \ + / sp.TCP(sport=4321, dport=1234, flags="S") + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + + ip = reply.getlayer(sp.IP) + tcp = reply.getlayer(sp.TCP) + assert ip + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert tcp + assert tcp.sport == 1234 + assert tcp.dport == 4321 + assert "R" in tcp.flags + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send a UDP packet, expect ICMP error + pkt = sp.IP(dst="198.51.100.2") \ + / sp.UDP(sport=4321, dport=1234) + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + ip = reply.getlayer(sp.IP) + icmp = reply.getlayer(sp.ICMP) + udp = reply.getlayer(sp.UDPerror) + + assert ip + assert ip.src == "192.0.2.1" + assert ip.dst == "192.0.2.2" + assert icmp + assert icmp.type == 3 + assert icmp.code == 3 + assert udp + assert udp.sport == 4321 + assert udp.dport == 1234 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_sctp(self): + self.common_setup() + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + # Send an SCTP init, expect an SCTP abort + pkt = sp.IP(dst="198.51.100.2") \ + / sp.SCTP(sport=1111, dport=2222) \ + / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500) + print(pkt) + reply = sp.sr1(pkt, timeout=3, verbose=False) + print(reply) + ip = reply.getlayer(sp.IP) + sctp = reply.getlayer(sp.SCTP) + abort = reply.getlayer(sp.SCTPChunkAbort) + print(sctp) + + assert ip + assert ip.src == "198.51.100.2" + assert ip.dst == "192.0.2.2" + assert sctp + assert sctp.sport == 2222 + assert sctp.dport == 1111 + assert(abort) diff --git a/tests/sys/netpfil/pf/ridentifier.sh b/tests/sys/netpfil/pf/ridentifier.sh index c456d2111e20..8d83bcfb8213 100644 --- a/tests/sys/netpfil/pf/ridentifier.sh +++ b/tests/sys/netpfil/pf/ridentifier.sh @@ -45,7 +45,7 @@ basic_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig lo0 up jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf # Sanity check atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2 @@ -56,7 +56,7 @@ basic_body() "pass in log" \ "pass in log proto tcp ridentifier 1234" - jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log & + jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log & sleep 1 echo "test" | nc -N 192.0.2.2 7 @@ -67,17 +67,17 @@ basic_body() # Make sure we spotted the ridentifier atf_check -s exit:0 -o ignore \ - grep 'rule 1/0.*ridentifier 1234' tcpdump.log + grep 'rule 1/0.*ridentifier 1234' ${PWD}/tcpdump.log # But not on the !TCP traffic atf_check -s exit:1 -o ignore \ - grep 'rule 0/0.*ridentifier' tcpdump.log + grep 'rule 0/0.*ridentifier' ${PWD}/tcpdump.log # Now try with antispoof rules pft_set_rules alcatraz \ "pass in log" \ "antispoof log for ${epair}b ridentifier 4321" - jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log & + jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log & sleep 1 # Without explicit rules for lo0 we're going to drop packets to ourself @@ -87,18 +87,16 @@ basic_body() sleep 1 jexec alcatraz killall tcpdump - cat tcpdump.log + cat ${PWD}/tcpdump.log # Make sure we spotted the ridentifier atf_check -s exit:0 -o ignore \ - grep 'rule 2/0.*ridentifier 4321' tcpdump.log + grep 'rule 2/0.*ridentifier 4321' ${PWD}/tcpdump.log } basic_cleanup() { pft_cleanup - rm -f inetd-alcatraz.pid - rm -f tcpdump.log } atf_init_test_cases() diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh index df95eaecc12e..5c0d355b8ea1 100644 --- a/tests/sys/netpfil/pf/route_to.sh +++ b/tests/sys/netpfil/pf/route_to.sh @@ -140,7 +140,7 @@ multiwan_body() jexec srv sysctl net.inet.ip.forwarding=1 # Run echo server in srv jail - jexec srv /usr/sbin/inetd -p multiwan.pid $(atf_get_srcdir)/echo_inetd.conf + jexec srv /usr/sbin/inetd -p ${PWD}/multiwan.pid $(atf_get_srcdir)/echo_inetd.conf jexec srv pfctl -e pft_set_rules srv \ @@ -178,7 +178,6 @@ multiwan_body() multiwan_cleanup() { - rm -f multiwan.pid pft_cleanup } @@ -257,7 +256,7 @@ icmp_nat_head() { atf_set descr 'Test that ICMP packets are correct for route-to + NAT' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } icmp_nat_body() @@ -517,6 +516,7 @@ ifbound_reply_to_head() { atf_set descr 'Test that reply-to states bind to the expected interface' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_body() @@ -572,6 +572,7 @@ ifbound_reply_to_v6_head() { atf_set descr 'Test that reply-to states bind to the expected interface for IPv6' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_v6_body() @@ -631,6 +632,7 @@ ifbound_reply_to_rdr_dummynet_head() { atf_set descr 'Test that reply-to states bind to the expected non-default-route interface after rdr and dummynet' atf_set require.user root + atf_set require.progs python3 scapy } ifbound_reply_to_rdr_dummynet_body() @@ -787,6 +789,76 @@ dummynet_double_cleanup() pft_cleanup } +atf_test_case "sticky" "cleanup" +sticky_head() +{ + atf_set descr 'Set and retrieve a rule with sticky-address' + atf_set require.user root +} + +sticky_body() +{ + pft_init + + vnet_mkjail alcatraz + + pft_set_rules alcatraz \ + "pass in quick log on n_test_h_rtr route-to (n_srv_h_rtr <change_dst>) sticky-address from any to <dst> keep state" + + jexec alcatraz pfctl -qvvsr +} + +sticky_cleanup() +{ + pft_cleanup +} + +atf_test_case "ttl" "cleanup" +ttl_head() +{ + atf_set descr 'Ensure we decrement TTL on route-to' + atf_set require.user root +} + +ttl_body() +{ + pft_init + + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + ifconfig ${epair_one}b 192.0.2.2/24 up + route add default 192.0.2.1 + + vnet_mkjail alcatraz ${epair_one}a ${epair_two}a + jexec alcatraz ifconfig ${epair_one}a 192.0.2.1/24 up + jexec alcatraz ifconfig ${epair_two}a 198.51.100.1/24 up + jexec alcatraz sysctl net.inet.ip.forwarding=1 + + vnet_mkjail singsing ${epair_two}b + jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up + jexec singsing route add default 198.51.100.1 + + # Sanity check + atf_check -s exit:0 -o ignore \ + ping -c 3 198.51.100.2 + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass out" \ + "pass in route-to (${epair_two}a 198.51.100.2)" + + atf_check -s exit:0 -o ignore \ + ping -c 3 198.51.100.2 + + atf_check -s exit:2 -o ignore \ + ping -m 1 -c 3 198.51.100.2 +} + +ttl_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "v4" @@ -803,4 +875,6 @@ atf_init_test_cases() atf_add_test_case "ifbound_reply_to_rdr_dummynet" atf_add_test_case "dummynet_frag" atf_add_test_case "dummynet_double" + atf_add_test_case "sticky" + atf_add_test_case "ttl" } diff --git a/tests/sys/netpfil/pf/rtable.sh b/tests/sys/netpfil/pf/rtable.sh index 62b37462e948..bb2cada57049 100644 --- a/tests/sys/netpfil/pf/rtable.sh +++ b/tests/sys/netpfil/pf/rtable.sh @@ -31,7 +31,7 @@ forward_v4_head() { atf_set descr 'Test IPv4 forwarding with rtable' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } forward_v4_body() @@ -78,7 +78,7 @@ forward_v6_head() { atf_set descr 'Test IPv6 forwarding with rtable' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } forward_v6_body() diff --git a/tests/sys/netpfil/pf/rules_counter.sh b/tests/sys/netpfil/pf/rules_counter.sh index 962e7c93ba93..98f96a7adca1 100644 --- a/tests/sys/netpfil/pf/rules_counter.sh +++ b/tests/sys/netpfil/pf/rules_counter.sh @@ -148,6 +148,54 @@ keepcounters_body() jexec alcatraz pfctl -s r -v } +atf_test_case "4G" "cleanup" +4G_head() +{ + atf_set descr 'Test keepcounter for values above 32 bits' + atf_set require.user root +} + +4G_body() +{ + pft_init + + epair=$(vnet_mkepair) + ifconfig ${epair}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair}b + jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up + jexec alcatraz nc -l 1234 >/dev/null & + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass all" + + # Now pass more than 4GB of data + dd if=/dev/zero bs=1k count=4M | nc -N 192.0.2.2 1234 + + bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }') + if [ $bytes -lt 4000000000 ]; + then + atf_fail "Expected to see > 4GB" + fi + + # Set new rules, keeping counters + pft_set_rules noflush alcatraz \ + "set keepcounters" \ + "pass all" + + bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }') + if [ $bytes -lt 4000000000 ]; + then + atf_fail "Expected to see > 4GB after rule reload" + fi +} + +4G_cleanup() +{ + pft_cleanup +} + keepcounters_cleanup() { pft_cleanup @@ -157,4 +205,5 @@ atf_init_test_cases() { atf_add_test_case "get_clear" atf_add_test_case "keepcounters" + atf_add_test_case "4G" } diff --git a/tests/sys/netpfil/pf/scrub.sh b/tests/sys/netpfil/pf/scrub.sh index b9efdaf5205c..6a5b748bed7b 100644 --- a/tests/sys/netpfil/pf/scrub.sh +++ b/tests/sys/netpfil/pf/scrub.sh @@ -32,7 +32,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -57,7 +57,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -82,7 +82,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -103,7 +103,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -124,7 +124,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -145,7 +145,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() @@ -166,7 +166,7 @@ no_scrub_v4_head() { atf_set descr 'Test IPv4 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v4_body() @@ -189,7 +189,7 @@ no_scrub_v6_head() { atf_set descr 'Test IPv6 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v6_body() diff --git a/tests/sys/netpfil/pf/scrub_compat.sh b/tests/sys/netpfil/pf/scrub_compat.sh index cf69da3f3b74..6e1499309869 100644 --- a/tests/sys/netpfil/pf/scrub_compat.sh +++ b/tests/sys/netpfil/pf/scrub_compat.sh @@ -33,7 +33,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -58,7 +58,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 scrub "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -83,7 +83,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -104,7 +104,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 scub "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -125,7 +125,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -146,7 +146,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 scub "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() @@ -167,7 +167,7 @@ no_scrub_v4_head() { atf_set descr 'Test IPv4 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v4_body() @@ -190,7 +190,7 @@ no_scrub_v6_head() { atf_set descr 'Test IPv6 "no scrub" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } no_scrub_v6_body() diff --git a/tests/sys/netpfil/pf/scrub_pass.sh b/tests/sys/netpfil/pf/scrub_pass.sh index 319d805144a0..8ba599144757 100644 --- a/tests/sys/netpfil/pf/scrub_pass.sh +++ b/tests/sys/netpfil/pf/scrub_pass.sh @@ -33,7 +33,7 @@ max_mss_v4_head() { atf_set descr 'Test IPv4 pass "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v4_body() @@ -58,7 +58,7 @@ max_mss_v6_head() { atf_set descr 'Test IPv6 pass "mss" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } max_mss_v6_body() @@ -83,7 +83,7 @@ set_tos_v4_head() { atf_set descr 'Test IPv4 pass "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v4_body() @@ -104,7 +104,7 @@ set_tos_v6_head() { atf_set descr 'Test IPv6 pass "set-tos" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } set_tos_v6_body() @@ -125,7 +125,7 @@ min_ttl_v4_head() { atf_set descr 'Test IPv4 pass "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v4_body() @@ -146,7 +146,7 @@ min_ttl_v6_head() { atf_set descr 'Test IPv6 pass "min-ttl" rule' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } min_ttl_v6_body() diff --git a/tests/sys/netpfil/pf/sctp.py b/tests/sys/netpfil/pf/sctp.py index 6042badffb64..da42ce527195 100644 --- a/tests/sys/netpfil/pf/sctp.py +++ b/tests/sys/netpfil/pf/sctp.py @@ -268,7 +268,8 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", - "pass inet proto sctp to 192.0.2.0/24"]) + "pass inet proto sctp to 192.0.2.0/24", + "pass on lo"]) # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) @@ -305,6 +306,7 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet proto sctp from 192.0.2.0/24"]) # Sanity check, we can communicate with the primary address. @@ -362,7 +364,7 @@ class TestSCTP(VnetTestTemplate): @pytest.mark.require_user("root") - def test_permutation(self): + def test_permutation_if_bound(self): # Test that we generate all permutations of src/dst addresses. # Assign two addresses to each end, and check for the expected states srv_vnet = self.vnet_map["vnet2"] @@ -374,6 +376,7 @@ class TestSCTP(VnetTestTemplate): ToolsHelper.pf_rules([ "set state-policy if-bound", "block proto sctp", + "pass on lo", "pass inet proto sctp to 192.0.2.0/24"]) # Sanity check, we can communicate with the primary address. @@ -387,11 +390,146 @@ class TestSCTP(VnetTestTemplate): # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4 states = ToolsHelper.get_output("/sbin/pfctl -ss") print(states) - assert re.search(r".*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states) + assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.2:1234", states) + + @pytest.mark.require_user("root") + def test_permutation_floating(self): + # Test that we generate all permutations of src/dst addresses. + # Assign two addresses to each end, and check for the expected states + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.4/24" % ifname) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass on lo", + "pass inet proto sctp to 192.0.2.0/24"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + print(states) + assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states) assert re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states) - assert re.search(r".*sctp 192.0.2.4:.*192.0.2.3:1234", states) + assert re.search(r"all sctp 192.0.2.4:.*192.0.2.3:1234", states) assert re.search(r"all sctp 192.0.2.4:.*192.0.2.2:1234", states) + @pytest.mark.require_user("root") + def test_limit_addresses(self): + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + for i in range(0, 16): + ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.%d/24" % (ifname, 4 + i)) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass on lo", + "pass inet proto sctp to 192.0.2.0/24"]) + + # Set up a connection, which will try to create states for all addresses + # we have assigned + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # But the number should be limited to 9 (original + 8 extra) + states = ToolsHelper.get_output("/sbin/pfctl -ss | grep 192.0.2.2") + print(states) + assert(states.count('\n') <= 9) + + @pytest.mark.require_user("root") + def test_disallow_related(self): + srv_vnet = self.vnet_map["vnet2"] + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "block proto sctp", + "pass inet proto sctp to 192.0.2.3", + "pass on lo"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # This shouldn't work + success=False + try: + client.newpeer("192.0.2.2") + client.send(b"world", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "world" + success=True + except: + success=False + assert not success + + # Check that we have a state for 192.0.2.3, but not 192.0.2.2 to 192.0.2.1 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert not re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states) + + @pytest.mark.require_user("root") + def test_allow_related(self): + srv_vnet = self.vnet_map["vnet2"] + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set state-policy if-bound", + "block proto sctp", + "pass inet proto sctp to 192.0.2.3 keep state (allow-related)", + "pass on lo"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("192.0.2.3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + success=False + try: + client.newpeer("192.0.2.2") + client.send(b"world", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "world" + success=True + finally: + # Debug output + ToolsHelper.print_output("/sbin/pfctl -ss") + ToolsHelper.print_output("/sbin/pfctl -sr -vv") + assert success + + # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states) + assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states) + class TestSCTPv6(VnetTestTemplate): REQUIRED_MODULES = ["sctp", "pf"] TOPOLOGY = { @@ -417,6 +555,7 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) # Sanity check, we can communicate with the primary address. @@ -454,6 +593,7 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp from 2001:db8::/64"]) # Sanity check, we can communicate with the primary address. @@ -520,7 +660,40 @@ class TestSCTPv6(VnetTestTemplate): ToolsHelper.print_output("/sbin/pfctl -e") ToolsHelper.pf_rules([ + "set state-policy if-bound", + "block proto sctp", + "pass on lo", + "pass inet6 proto sctp to 2001:db8::0/64"]) + + # Sanity check, we can communicate with the primary address. + client = SCTPClient("2001:db8::3", 1234) + client.send(b"hello", 0) + rcvd = self.wait_object(srv_vnet.pipe) + print(rcvd) + assert rcvd['ppid'] == 0 + assert rcvd['data'] == "hello" + + # Check that we have a state for 2001:db8::3 and 2001:db8::2 to 2001:db8::1, but also to 2001:db8::4 + states = ToolsHelper.get_output("/sbin/pfctl -ss") + print(states) + assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::2\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::3\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::2\[1234\]", states) + assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::3\[1234\]", states) + + @pytest.mark.require_user("root") + def test_permutation_floating(self): + # Test that we generate all permutations of src/dst addresses. + # Assign two addresses to each end, and check for the expected states + srv_vnet = self.vnet_map["vnet2"] + + ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/ifconfig %s inet6 alias 2001:db8::4/64" % ifname) + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ "block proto sctp", + "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) # Sanity check, we can communicate with the primary address. diff --git a/tests/sys/netpfil/pf/sctp.sh b/tests/sys/netpfil/pf/sctp.sh index 66225e132d1c..57dcdad1d866 100644 --- a/tests/sys/netpfil/pf/sctp.sh +++ b/tests/sys/netpfil/pf/sctp.sh @@ -181,6 +181,64 @@ basic_v6_cleanup() pft_cleanup } +atf_test_case "reuse" "cleanup" +reuse_head() +{ + atf_set descr 'Test handling dumb clients that reuse source ports' + atf_set require.user root +} + +reuse_body() +{ + sctp_init + + j="sctp:reuse" + epair=$(vnet_mkepair) + + vnet_mkjail ${j}a ${epair}a + vnet_mkjail ${j}b ${epair}b + + jexec ${j}a ifconfig ${epair}a 192.0.2.1/24 up + jexec ${j}b ifconfig ${epair}b 192.0.2.2/24 up + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec ${j}a ping -c 1 192.0.2.2 + + jexec ${j}a pfctl -e + pft_set_rules ${j}a \ + "block" \ + "pass in proto sctp to port 1234" + + echo "foo" | jexec ${j}a nc --sctp -N -l 1234 & + + # Wait for the server to start + sleep 1 + + out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234) + if [ "$out" != "foo" ]; then + atf_fail "SCTP connection failed" + fi + + # Now do the same thing again, with the same port numbers + jexec ${j}a pfctl -ss -v + + echo "foo" | jexec ${j}a nc --sctp -N -l 1234 & + + # Wait for the server to start + sleep 1 + + out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234) + if [ "$out" != "foo" ]; then + atf_fail "SCTP connection failed" + fi + jexec ${j}a pfctl -ss -v +} + +reuse_cleanup() +{ + pft_cleanup +} + atf_test_case "abort_v4" "cleanup" abort_v4_head() { @@ -504,6 +562,7 @@ pfsync_body() sctp_init pfsynct_init + vnet_init_bridge if ! kldstat -q -m carp then atf_skip "This test requires carp" @@ -655,14 +714,133 @@ pfsync_cleanup() pfsynct_cleanup } +atf_test_case "timeout" "cleanup" +timeout_head() +{ + atf_set descr 'Test setting and retrieving timeout values' + atf_set require.user root +} + +timeout_body() +{ + sctp_init + + vnet_mkjail timeout + + pft_set_rules timeout \ + "set timeout sctp.first 13" \ + "set timeout sctp.opening 14" + + atf_check -s exit:0 -o match:"sctp.first.*13" \ + jexec timeout pfctl -st + atf_check -s exit:0 -o match:"sctp.opening.*14" \ + jexec timeout pfctl -st + # We've not changed other timeouts + atf_check -s exit:0 -o match:"sctp.established.*86400" \ + jexec timeout pfctl -st +} + +timeout_cleanup() +{ + pft_cleanup +} + +atf_test_case "related_icmp" "cleanup" +related_icmp_head() +{ + atf_set descr 'Verify that ICMP messages related to an SCTP connection are allowed' + atf_set require.user root +} + +related_icmp_body() +{ + sctp_init + + epair_cl=$(vnet_mkepair) + epair_rtr=$(vnet_mkepair) + epair_srv=$(vnet_mkepair) + + ifconfig ${epair_cl}a 192.0.2.1/24 up + route add default 192.0.2.2 + + vnet_mkjail rtr ${epair_cl}b ${epair_rtr}a + jexec rtr ifconfig ${epair_cl}b 192.0.2.2/24 up + jexec rtr ifconfig ${epair_rtr}a 198.51.100.1/24 up + jexec rtr sysctl net.inet.ip.forwarding=1 + jexec rtr route add default 198.51.100.2 + + vnet_mkjail rtr2 ${epair_rtr}b ${epair_srv}a + jexec rtr2 ifconfig ${epair_rtr}b 198.51.100.2/24 up + jexec rtr2 ifconfig ${epair_srv}a 203.0.113.1/24 up + jexec rtr2 ifconfig ${epair_srv}a mtu 1300 + jexec rtr2 sysctl net.inet.ip.forwarding=1 + jexec rtr2 route add default 198.51.100.1 + + vnet_mkjail srv ${epair_srv}b + jexec srv ifconfig ${epair_srv}b 203.0.113.2/24 up + jexec srv ifconfig ${epair_srv}b mtu 1300 + jexec srv route add default 203.0.113.1 + + # Sanity checks + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 203.0.113.2 + + jexec rtr pfctl -e + pft_set_rules rtr \ + "block proto icmp" \ + "pass proto sctp" + + # Make sure SCTP traffic passes + echo "foo" | jexec srv nc --sctp -N -l 1234 & + sleep 1 + + out=$(nc --sctp -N -w 3 203.0.113.2 1234) + if [ "$out" != "foo" ]; then + jexec rtr pfctl -ss -vv + jexec rtr pfctl -sr -vv + atf_fail "SCTP connection failed" + fi + + # Do we see ICMP traffic if we send overly large traffic? + echo "foo" | jexec srv nc --sctp -l 1234 >/dev/null & + sleep 1 + + atf_check -s exit:0 -o not-match:".*destination unreachable:.*" \ + netstat -s -p icmp + + # Generate traffic that will be fragmented by rtr2, and will provoke an + # ICMP unreachable - need to frag (mtu 1300) message + dd if=/dev/random bs=10000 count=1 | nc --sctp -N -w 3 203.0.113.2 1234 + + # We'd expect to see an ICMP message + atf_check -s exit:0 -o match:".*destination unreachable: [1-9]" \ + netstat -s -p icmp +} + +related_icmp_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "basic_v4" atf_add_test_case "basic_v6" + atf_add_test_case "reuse" atf_add_test_case "abort_v4" atf_add_test_case "abort_v6" atf_add_test_case "nat_v4" atf_add_test_case "nat_v6" atf_add_test_case "rdr_v4" atf_add_test_case "pfsync" + atf_add_test_case "timeout" + atf_add_test_case "related_icmp" } diff --git a/tests/sys/netpfil/pf/set_skip.sh b/tests/sys/netpfil/pf/set_skip.sh index e5b1440360e9..e984377721b8 100644 --- a/tests/sys/netpfil/pf/set_skip.sh +++ b/tests/sys/netpfil/pf/set_skip.sh @@ -26,6 +26,50 @@ . $(atf_get_srcdir)/utils.subr +atf_test_case "unset" "cleanup" +unset_head() +{ + atf_set descr 'Unset set skip test' + atf_set require.user root +} + +unset_body() +{ + pft_init + + vnet_mkjail alcatraz + jexec alcatraz ifconfig lo0 127.0.0.1/8 up + jexec alcatraz pfctl -e + pft_set_rules alcatraz "set skip on lo0" \ + "block in proto icmp" + + echo "set skip" + jexec alcatraz pfctl -v -sI + + jexec alcatraz ifconfig + atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + # Unset the skip on the group + pft_set_rules noflush alcatraz \ + "block in proto icmp" + + echo "No setskip" + jexec alcatraz pfctl -v -sI + + # Do flush states + jexec alcatraz pfctl -Fs + + # And now our ping is blocked + atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + jexec alcatraz pfctl -v -sI +} + +unset_cleanup() +{ + pft_cleanup +} + atf_test_case "set_skip_group" "cleanup" set_skip_group_head() { @@ -45,8 +89,24 @@ set_skip_group_body() pft_set_rules alcatraz "set skip on foo" \ "block in proto icmp" + echo "set skip" + jexec alcatraz pfctl -v -sI + jexec alcatraz ifconfig atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + # Unset the skip on the group + pft_set_rules noflush alcatraz \ + "block in proto icmp" + + # Do flush states + jexec alcatraz pfctl -Fs + + # And now our ping is blocked + atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1 + + echo "No setskip" + jexec alcatraz pfctl -v -sI } set_skip_group_cleanup() @@ -163,6 +223,7 @@ pr255852_cleanup() atf_init_test_cases() { + atf_add_test_case "unset" atf_add_test_case "set_skip_group" atf_add_test_case "set_skip_group_lo" atf_add_test_case "set_skip_dynamic" diff --git a/tests/sys/netpfil/pf/set_tos.sh b/tests/sys/netpfil/pf/set_tos.sh index bfec61f0d221..75b96edbab6e 100644 --- a/tests/sys/netpfil/pf/set_tos.sh +++ b/tests/sys/netpfil/pf/set_tos.sh @@ -37,7 +37,7 @@ v4_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v4_body() @@ -122,7 +122,7 @@ v6_head() atf_set require.user root # We need scapy to be installed for out test scripts to work - atf_set require.progs scapy + atf_set require.progs python3 scapy } v6_body() @@ -191,6 +191,22 @@ v6_body() --to 2001:db8:192::2 \ --replyif ${epair}a \ --expect-tc 0 + + # We can set tos on pass rules + pft_set_rules alcatraz "pass out set tos 13" + atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8:192::2 \ + --replyif ${epair}a \ + --expect-tc 13 + + # And that still works with 'scrub' options too + pft_set_rules alcatraz "pass out set tos 14 scrub (min-ttl 64)" + atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \ + --sendif ${epair}a \ + --to 2001:db8:192::2 \ + --replyif ${epair}a \ + --expect-tc 14 } v6_cleanup() diff --git a/tests/sys/netpfil/pf/snmp.sh b/tests/sys/netpfil/pf/snmp.sh new file mode 100644 index 000000000000..37cc4b75cf92 --- /dev/null +++ b/tests/sys/netpfil/pf/snmp.sh @@ -0,0 +1,123 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 Kristof Provost <kp@FreeBSD.org> +# +# 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic pf_snmp test' + atf_set require.user root +} + +basic_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + # Start bsnmpd + jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass" + + # Sanity check, and create state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # pf should be enabled + atf_check -s exit:0 -o match:'pfStatusRunning.0 = true' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_test_case "table" "cleanup" +table_head() +{ + atf_set descr 'Test tables and pf_snmp' + atf_set require.user root +} + +table_body() +{ + pft_init + + epair=$(vnet_mkepair) + + ifconfig ${epair}b 192.0.2.2/24 up + + vnet_mkjail alcatraz ${epair}a + jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.0/24 }" \ + "pass in from <foo>" + + # Start bsnmpd after creating the table so we don't have to wait for + # a refresh timeout + jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf + + # Sanity check, and create state + atf_check -s exit:0 -o ignore \ + ping -c 1 192.0.2.1 + + # We should have one table + atf_check -s exit:0 -o match:'pfTablesTblNumber.0 = 1' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot + + # We have the 'foo' table + atf_check -s exit:0 -o match:'pfTablesTblDescr.* = foo' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables + + # Which contains address 192.0.2.0/24 + atf_check -s exit:0 -o match:'pfTablesAddrNet.* = 192.0.2.0' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables + atf_check -s exit:0 -o match:'pfTablesAddrPrefix.* = 24' \ + bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables +} + +table_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" + atf_add_test_case "table" +} diff --git a/tests/sys/netpfil/pf/src_track.sh b/tests/sys/netpfil/pf/src_track.sh index 27eb62abcf41..c24f88062c4d 100755 --- a/tests/sys/netpfil/pf/src_track.sh +++ b/tests/sys/netpfil/pf/src_track.sh @@ -2,6 +2,7 @@ # SPDX-License-Identifier: BSD-2-Clause # # Copyright (c) 2020 Kristof Provost <kp@FreeBSD.org> +# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net> # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions @@ -51,7 +52,19 @@ source_track_body() "pass out keep state (source-track)" ping -c 3 192.0.2.1 - jexec alcatraz pfctl -s all -v + atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # Flush all source nodes + jexec alcatraz pfctl -FS + + # We can't find the previous source node any more + atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # But we still have the state + atf_check -s exit:0 -o match:'all icmp 192.0.2.1:8 <- 192.0.2.2:.*' \ + jexec alcatraz pfctl -ss } source_track_cleanup() @@ -59,7 +72,444 @@ source_track_cleanup() pft_cleanup } +atf_test_case "kill" "cleanup" +kill_head() +{ + atf_set descr 'Test killing source nodes' + atf_set require.user root +} + +kill_body() +{ + pft_init + + epair=$(vnet_mkepair) + vnet_mkjail alcatraz ${epair}b + + ifconfig ${epair}a 192.0.2.2/24 up + ifconfig ${epair}a inet alias 192.0.2.3/24 up + jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up + + # Enable pf! + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "pass in keep state (source-track)" \ + "pass out keep state (source-track)" + + # Establish two sources + atf_check -s exit:0 -o ignore \ + ping -c 1 -S 192.0.2.2 192.0.2.1 + atf_check -s exit:0 -o ignore \ + ping -c 1 -S 192.0.2.3 192.0.2.1 + + # Check that both source nodes exist + atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + +jexec alcatraz pfctl -sS + + # Kill the 192.0.2.2 source + jexec alcatraz pfctl -K 192.0.2.2 + + # The other source still exists + atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS + + # But not the one we killed + atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \ + jexec alcatraz pfctl -sS +} + +kill_cleanup() +{ + pft_cleanup +} + +max_src_conn_rule_head() +{ + atf_set descr 'Max connections per source per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_conn_rule_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp keep state (max-src-conn 3 source-track rule overload <bad_hosts>)" \ + "pass out on ${epair_server}a inet6 proto tcp keep state" + + # Limiting of connections is done for connections which have successfully + # finished the 3-way handshake. Once the handshake is done, the state + # is moved to CLOSED state. We use pft_ping.py to check that the handshake + # was really successful and after that we check what is in pf state table. + + # 3 connections from host ::1 will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4202 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4203 --fromaddr 2001:db8:44::1 + # The 4th connection from host ::1 will have its state killed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4204 --fromaddr 2001:db8:44::1 + # A connection from host :2 is will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4205 --fromaddr 2001:db8:44::2 + + states=$(mktemp) || exit 1 + jexec router pfctl -qss | normalize_pfctl_s | grep 'tcp 2001:db8:43::2\[9\] <-' > $states + + grep -qE '2001:db8:44::1\[4201\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4201 not found or not established" + grep -qE '2001:db8:44::1\[4202\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4202 not found or not established" + grep -qE '2001:db8:44::1\[4203\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4203 not found or not established" + grep -qE '2001:db8:44::2\[4205\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4205 not found or not established" + + if ( + grep -qE '2001:db8:44::1\[4204\] ' $states && + ! grep -qE '2001:db8:44::1\[4204\] CLOSED:CLOSED' $states + ); then + atf_fail "State for port 4204 found but not closed" + fi + + jexec router pfctl -T test -t bad_hosts 2001:db8:44::1 || atf_fail "Host not found in overload table" +} + +max_src_conn_rule_cleanup() +{ + pft_cleanup +} + +max_src_states_rule_head() +{ + atf_set descr 'Max states per source per rule' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_states_rule_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track rule) label rule_A" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track rule) label rule_B" \ + "pass out on ${epair_server}a keep state" + + # The option max-src-states prevents even the initial SYN packet going + # through. It's enough that we check ping_server_check_reply, no need to + # bother checking created states. + + # 2 connections from host ::1 matching rule_A will be allowed, 1 will fail to create a state. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1 + + # 2 connections from host ::1 matching rule_B will be allowed, 1 will fail to create a state. + # Limits from rule_A don't interfere with rule_B. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4222 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4223 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::1 + + # 2 connections from host ::2 matching rule_B will be allowed, 1 will fail to create a state. + # Limits for host ::1 will not interfere with host ::2. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4225 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4226 --fromaddr 2001:db8:44::2 + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4227 --fromaddr 2001:db8:44::2 + + # We will check the resulting source nodes, though. + # Order of source nodes in output is not guaranteed, find each one separately. + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvsS | normalize_pfctl_s > $nodes + for node_regexp in \ + '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 3, limit source-track$' \ + '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \ + '2001:db8:44::2 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done + + # Check if limit counters have been properly set. + jexec router pfctl -qvvsi | grep -qE 'max-src-states\s+3\s+' || atf_fail "max-src-states not set to 3" +} + +max_src_states_rule_cleanup() +{ + pft_cleanup +} + +max_src_states_global_head() +{ + atf_set descr 'Max states per source global' + atf_set require.user root + atf_set require.progs python3 scapy +} + +max_src_states_global_body() +{ + setup_router_server_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses and for tester jail + # to not respond with RST packets for SYN+ACKs. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1 + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track global) label rule_A" \ + "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track global) label rule_B" \ + "pass out on ${epair_server}a keep state" + + # Global source tracking creates a single source node shared between all + # rules for each connecting source IP address and counts states created + # by all rules. Each rule has its own max-src-conn value checked against + # that single source node. + + # 3 connections from host …::1 matching rule_A will be allowed. + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1 + ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1 + # The 4th connection matching rule_A from host …::1 will have its state killed. + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1 + # A connection matching rule_B from host …::1 will have its state killed too. + ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1 + + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvsS | normalize_pfctl_s > $nodes + cat $nodes + node_regexp='2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, limit source-track' + grep -qE "$node_regexp" $nodes || atf_fail "Source nodes not matching expected output" +} + +max_src_states_global_cleanup() +{ + pft_cleanup +} + +sn_types_compat_head() +{ + atf_set descr 'Combination of source node types with compat NAT rules' + atf_set require.user root + atf_set require.progs python3 scapy +} + +sn_types_compat_body() +{ + setup_router_dummy_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + + # Additional gateways for route-to. + rtgw=${net_server_host_server%::*}::2:1 + jexec router ndp -s ${rtgw} 00:01:02:03:04:05 + + # This test will check for proper source node creation for: + # max-src-states -> PF_SN_LIMIT + # sticky-address -> PF_SN_NAT + # route-to -> PF_SN_ROUTE + # The test expands to all 8 combinations of those source nodes being + # present or not. + + pft_set_rules router \ + "table <rtgws> { ${rtgw} }" \ + "table <rdrgws> { 2001:db8:45::1 }" \ + "rdr on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 -> <rdrgws> port 4242 sticky-address" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_3" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_4" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_5" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_6" \ + "pass out quick on ${epair_server}a keep state" + + # We don't check if state limits are properly enforced, this is tested + # by other tests in this file. + # Source address will not match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1 + # Source address will match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes + + # Order of states in output is not guaranteed, find each one separately. + for state_regexp in \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address, route sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address, route sticky-address' \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Order of source nodes in output is not guaranteed, find each one separately. + for node_regexp in \ + '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \ + '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \ + '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \ + '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \ + '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \ + '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \ + '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \ + '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done + + ! grep -q 'filter rule 3' $nodes || atf_fail "Source node found for rule 3" +} + +sn_types_compat_cleanup() +{ + pft_cleanup +} + +sn_types_pass_head() +{ + atf_set descr 'Combination of source node types with pass NAT rules' + atf_set require.user root + atf_set require.progs python3 scapy +} + +sn_types_pass_body() +{ + setup_router_dummy_ipv6 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2 + + # Additional gateways for route-to. + rtgw=${net_server_host_server%::*}::2:1 + jexec router ndp -s ${rtgw} 00:01:02:03:04:05 + + # This test will check for proper source node creation for: + # max-src-states -> PF_SN_LIMIT + # sticky-address -> PF_SN_NAT + # route-to -> PF_SN_ROUTE + # The test expands to all 8 combinations of those source nodes being + # present or not. + + pft_set_rules router \ + "table <rtgws> { ${rtgw} }" \ + "table <rdrgws> { 2001:db8:45::1 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "match in on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 rdr-to <rdrgws> port 4242 sticky-address label rule_3" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_4" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_5" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_6" \ + "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_7" \ + "pass out quick on ${epair_server}a keep state" + + # We don't check if state limits are properly enforced, this is tested + # by other tests in this file. + # Source address will not match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1 + # Source address will match the NAT rule + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1 + ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvss | normalize_pfctl_s > $states + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes + + echo " === states ===" + cat $states + echo " === nodes ===" + cat $nodes + echo " === end === " + + # Order of states in output is not guaranteed, find each one separately. + for state_regexp in \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track$' \ + 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, route sticky-address$' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, NAT/RDR sticky-address, route sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address' \ + 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, NAT/RDR sticky-address, route sticky-address' \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Order of source nodes in output is not guaranteed, find each one separately. + for node_regexp in \ + '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \ + '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \ + '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \ + '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \ + '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \ + '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \ + '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \ + '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done +} + +sn_types_pass_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "source_track" + atf_add_test_case "kill" + atf_add_test_case "max_src_conn_rule" + atf_add_test_case "max_src_states_rule" + atf_add_test_case "max_src_states_global" + atf_add_test_case "sn_types_compat" + atf_add_test_case "sn_types_pass" } diff --git a/tests/sys/netpfil/pf/status.sh b/tests/sys/netpfil/pf/status.sh new file mode 100644 index 000000000000..bfd916a40c01 --- /dev/null +++ b/tests/sys/netpfil/pf/status.sh @@ -0,0 +1,73 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2024 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. + +. $(atf_get_srcdir)/utils.subr + +atf_test_case "basic" "cleanup" +basic_head() +{ + atf_set descr 'Basic get/clear status test case' + atf_set require.user root +} + +basic_body() +{ + pft_init + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + jexec one ifconfig ${epair}a 192.0.2.1/24 up + vnet_mkjail two ${epair}b + jexec two ifconfig ${epair}b 192.0.2.2/24 up + + jexec one pfctl -e + pft_set_rules one "pass" + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 192.0.2.1 + + atf_check -s exit:0 -o not-match:'searches[[:space:]]+0' \ + jexec one pfctl -si + + atf_check -s exit:0 -o ignore -e ignore \ + jexec one pfctl -Fi + + atf_check -s exit:0 -o match:'searches[[:space:]]+0' \ + jexec one pfctl -si +} + +basic_cleanup() +{ + pft_cleanup +} + +atf_init_test_cases() +{ + atf_add_test_case "basic" +} + diff --git a/tests/sys/netpfil/pf/syncookie.sh b/tests/sys/netpfil/pf/syncookie.sh index 8feb2816f589..fad90f3b2618 100644 --- a/tests/sys/netpfil/pf/syncookie.sh +++ b/tests/sys/netpfil/pf/syncookie.sh @@ -51,7 +51,7 @@ basic_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a 192.0.2.2/24 up @@ -81,7 +81,7 @@ basic_body() basic_cleanup() { - rm -f inetd-alcatraz.pid + rm -f ${PWD}/inetd-alcatraz.pid pft_cleanup } @@ -100,7 +100,7 @@ basic_v6_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad @@ -130,7 +130,6 @@ basic_v6_body() basic_v6_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -157,7 +156,7 @@ forward_body() jexec srv ifconfig ${epair_out}b 198.51.100.2/24 up jexec srv route add default 198.51.100.1 - jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair_in}a 192.0.2.2/24 up @@ -181,7 +180,6 @@ forward_body() forward_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -208,7 +206,7 @@ forward_v6_body() jexec srv ifconfig ${epair_out}b inet6 2001:db8:1::2/64 up no_dad jexec srv route -6 add default 2001:db8:1::1 - jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair_in}a inet6 2001:db8::2/64 up no_dad @@ -232,7 +230,90 @@ forward_v6_body() forward_v6_cleanup() { - rm -f inetd-alcatraz.pid + pft_cleanup +} + +loopback_test() +{ + local addr port + + addr=$1 + port=$2 + + # syncookies don't work without state tracking enabled. + atf_check -e ignore pfctl -e + atf_check pfctl -f - <<__EOF__ +set syncookies always +pass all keep state +__EOF__ + + # Try to transmit data over a loopback connection. + cat <<__EOF__ >in +Creativity, no. +__EOF__ + nc -l $addr $port >out & + + atf_check nc -N $addr $port < in + + atf_check -o file:in cat out + + atf_check -e ignore pfctl -d +} + +atf_test_case "loopback" "cleanup" +loopback_head() +{ + atf_set descr 'Make sure that loopback v4 TCP connections work with syncookies on' + atf_set require.user root +} + +loopback_body() +{ + local epair + + pft_init + + atf_check ifconfig lo0 127.0.0.1/8 + atf_check ifconfig lo0 up + + loopback_test 127.0.0.1 8080 + + epair=$(vnet_mkepair) + atf_check ifconfig ${epair}a inet 192.0.2.1/24 + + loopback_test 192.0.2.1 8081 +} + +loopback_cleanup() +{ + pft_cleanup +} + +atf_test_case "loopback_v6" "cleanup" +loopback_v6_head() +{ + atf_set descr 'Make sure that loopback v6 TCP connections work with syncookies on' + atf_set require.user root +} + +loopback_v6_body() +{ + local epair + + pft_init + + atf_check ifconfig lo0 up + + loopback_test ::1 8080 + + epair=$(vnet_mkepair) + atf_check ifconfig ${epair}a inet6 2001:db8::1/64 + + loopback_test 2001:db8::1 8081 +} + +loopback_v6_cleanup() +{ pft_cleanup } @@ -241,7 +322,7 @@ nostate_head() { atf_set descr 'Ensure that we do not create until SYN|ACK' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nostate_body() @@ -287,7 +368,7 @@ nostate_v6_head() { atf_set descr 'Ensure that we do not create until SYN|ACK' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } nostate_v6_body() @@ -334,7 +415,7 @@ adaptive_head() { atf_set descr 'Adaptive mode test' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } adaptive_body() @@ -440,7 +521,7 @@ port_reuse_body() vnet_mkjail alcatraz ${epair}b vnet_mkjail singsing jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p ${HOME}/inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf ifconfig ${epair}a 192.0.2.2/24 up @@ -486,6 +567,8 @@ atf_init_test_cases() atf_add_test_case "basic_v6" atf_add_test_case "forward" atf_add_test_case "forward_v6" + atf_add_test_case "loopback" + atf_add_test_case "loopback_v6" atf_add_test_case "nostate" atf_add_test_case "nostate_v6" atf_add_test_case "adaptive" diff --git a/tests/sys/netpfil/pf/synproxy.sh b/tests/sys/netpfil/pf/synproxy.sh index 3b3dc62b8993..617fa6ba2afc 100644 --- a/tests/sys/netpfil/pf/synproxy.sh +++ b/tests/sys/netpfil/pf/synproxy.sh @@ -52,7 +52,7 @@ synproxy_body() jexec singsing ifconfig ${link}b 198.51.100.2/24 up jexec singsing route add default 198.51.100.1 - jexec singsing /usr/sbin/inetd -p inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf + jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e pft_set_rules alcatraz "set fail-policy return" \ @@ -74,7 +74,6 @@ synproxy_body() synproxy_cleanup() { - rm -f inetd-singsing.pid pft_cleanup } @@ -94,7 +93,7 @@ local_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e @@ -115,7 +114,6 @@ local_body() local_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } @@ -135,7 +133,7 @@ local_v6_body() vnet_mkjail alcatraz ${epair}b jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2/64 up - jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \ + jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \ $(atf_get_srcdir)/echo_inetd.conf jexec alcatraz pfctl -e @@ -155,7 +153,6 @@ local_v6_body() local_v6_cleanup() { - rm -f inetd-alcatraz.pid pft_cleanup } diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh index 32943e659bd0..78320375db7c 100644 --- a/tests/sys/netpfil/pf/table.sh +++ b/tests/sys/netpfil/pf/table.sh @@ -109,6 +109,268 @@ v6_counters_cleanup() pft_cleanup } +atf_test_case "match_counters" "cleanup" +match_counters_head() +{ + atf_set descr 'Test that counters for tables in match rules work' + atf_set require.user root +} + +match_counters_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1 }" \ + "pass all" \ + "match in from <foo> to any" \ + "match out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 192.0.2.2 + + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +match_counters_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero_one" "cleanup" +zero_one_head() +{ + atf_set descr 'Test zeroing a single address in a table' + atf_set require.user root +} + +pft_cleared_ctime() +{ + jexec "$1" pfctl -t "$2" -vvT show | awk -v ip="$3" ' + ($1 == ip) { m = 1 } + ($1 == "Cleared:" && m) { + sub("[[:space:]]*Cleared:[[:space:]]*", ""); print; exit }' +} + +ctime_to_unixtime() +{ + # NB: it's not TZ=UTC, it's TZ=/etc/localtime + date -jf '%a %b %d %H:%M:%S %Y' "$1" '+%s' +} + +zero_one_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2 + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + jexec alcatraz pfctl -t foo -T show -vv + + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv + + local uniq base ts1 ts3 + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal 1 "$uniq" # time they were added + + base=`pft_cleared_ctime alcatraz foo 192.0.2.1` + + atf_check -s exit:0 -e ignore \ + jexec alcatraz pfctl -t foo -T zero 192.0.2.3 + + ts1=`pft_cleared_ctime alcatraz foo 192.0.2.1` + atf_check_equal "$base" "$ts1" + + ts3=`pft_cleared_ctime alcatraz foo 192.0.2.3` + atf_check test "$ts1" != "$ts3" + + ts1=`ctime_to_unixtime "$ts1"` + ts3=`ctime_to_unixtime "$ts3"` + atf_check test $(( "$ts3" - "$ts1" )) -lt 10 # (3 pings * 2) + epsilon + atf_check test "$ts1" -lt "$ts3" + + # We now have a zeroed and a non-zeroed counter, so both patterns + # should match + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +zero_one_cleanup() +{ + pft_cleanup +} + +atf_test_case "zero_all" "cleanup" +zero_all_head() +{ + atf_set descr 'Test zeroing all table entries' + atf_set require.user root +} + +zero_all_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "set skip on lo" + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2 + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + jexec alcatraz pfctl -t foo -T show -vv + atf_check -s exit:0 -e ignore \ + -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv + + atf_check -s exit:0 -e ignore \ + jexec alcatraz pfctl -t foo -T zero + + jexec alcatraz pfctl -t foo -T show -vv + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -T show -vv +} + +zero_all_cleanup() +{ + pft_cleanup +} + +atf_test_case "reset_nonzero" "cleanup" +reset_nonzero_head() +{ + atf_set descr 'Test zeroing an address with non-zero counters' + atf_set require.user root +} + +reset_nonzero_body() +{ + pft_init + + epair_send=$(vnet_mkepair) + ifconfig ${epair_send}a 192.0.2.1/24 up + ifconfig ${epair_send}a inet alias 192.0.2.3/24 + + vnet_mkjail alcatraz ${epair_send}b + jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up + jexec alcatraz pfctl -e + + pft_set_rules alcatraz \ + "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \ + "table <bar> counters { }" \ + "block all" \ + "pass in from <foo> to any" \ + "pass out from any to <foo>" \ + "pass on notReallyAnIf from <bar> to <bar>" \ + "set skip on lo" + + # Nonexisting table can't be reset, following `-T show`. + atf_check -o ignore \ + -s not-exit:0 \ + -e inline:"pfctl: Table does not exist.\n" \ + jexec alcatraz pfctl -t nonexistent -T reset + + atf_check -o ignore \ + -s exit:0 \ + -e inline:"0/0 stats cleared.\n" \ + jexec alcatraz pfctl -t bar -T reset + + # No-op is a valid operation. + atf_check -s exit:0 \ + -e inline:"0/2 stats cleared.\n" \ + jexec alcatraz pfctl -t foo -T reset + + atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2 + + atf_check -s exit:0 -e ignore \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -vvT show + + local clrd uniq + clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared` + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal "$clrd" 2 + atf_check_equal "$uniq" 1 # time they were added + + atf_check -s exit:0 -e ignore \ + -e inline:"1/2 stats cleared.\n" \ + jexec alcatraz pfctl -t foo -T reset + + clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared` + uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared` + atf_check_equal "$clrd" 2 + atf_check_equal "$uniq" 2 # 192.0.2.3 should get new timestamp + + atf_check -s exit:0 -e ignore \ + -o not-match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o not-match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \ + -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \ + jexec alcatraz pfctl -t foo -vvT show +} + +reset_nonzero_cleanup() +{ + pft_cleanup +} + atf_test_case "pr251414" "cleanup" pr251414_head() { @@ -324,6 +586,10 @@ atf_init_test_cases() { atf_add_test_case "v4_counters" atf_add_test_case "v6_counters" + atf_add_test_case "match_counters" + atf_add_test_case "zero_one" + atf_add_test_case "zero_all" + atf_add_test_case "reset_nonzero" atf_add_test_case "pr251414" atf_add_test_case "automatic" atf_add_test_case "network" diff --git a/tests/sys/netpfil/pf/tcp.py b/tests/sys/netpfil/pf/tcp.py new file mode 100644 index 000000000000..53e0658f419c --- /dev/null +++ b/tests/sys/netpfil/pf/tcp.py @@ -0,0 +1,158 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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 sys +import pytest +import random +import socket +import selectors +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TCPClient: + def __init__(self, src, dst, sport, dport, sp): + self.src = src + self.dst = dst + self.sport = sport + self.dport = dport + self.sp = sp + self.seq = random.randrange(1, (2**32)-1) + self.ack = 0 + + def syn(self): + syn = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, flags="S", seq=self.seq) + return syn + + def connect(self): + syn = self.syn() + r = self.sp.sr1(syn, timeout=5) + + assert r + t = r.getlayer(self.sp.TCP) + assert t + assert t.sport == self.dport + assert t.dport == self.sport + assert t.flags == "SA" + + self.seq += 1 + self.ack = t.seq + 1 + ack = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, flags="A", ack=self.ack, seq=self.seq) + self.sp.send(ack) + + def send(self, data): + length = len(data) + pkt = self.sp.IP(src=self.src, dst=self.dst) \ + / self.sp.TCP(sport=self.sport, dport=self.dport, ack=self.ack, seq=self.seq, flags="") \ + / self.sp.Raw(data) + self.seq += length + pkt.show() + self.sp.send(pkt) + +class TestTcp(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]}, + } + + def vnet2_handler(self, vnet): + ToolsHelper.print_output("/usr/sbin/arp -s 192.0.2.3 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass" + ]) + ToolsHelper.print_output("/sbin/pfctl -x loud") + + # Start TCP listener + sel = selectors.DefaultSelector() + t = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + t.bind(("0.0.0.0", 1234)) + t.listen(100) + t.setblocking(False) + sel.register(t, selectors.EVENT_READ, data=None) + + while True: + events = sel.select(timeout=2) + for key, mask in events: + sock = key.fileobj + if key.data is None: + conn, addr = sock.accept() + print(f"Accepted connection from {addr}") + events = selectors.EVENT_READ | selectors.EVENT_WRITE + sel.register(conn, events, data="TCP") + else: + if mask & selectors.EVENT_READ: + recv_data = sock.recv(1024) + print(f"Received TCP {recv_data}") + ToolsHelper.print_output("/sbin/pfctl -ss -vv") + sock.send(recv_data) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_challenge_ack(self): + vnet = self.vnet_map["vnet1"] + ifname = vnet.iface_alias_map["if1"].name + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + a = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp) + a.connect() + a.send(b"foo") + + b = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp) + syn = b.syn() + syn.show() + s = DelayedSend(syn) + packets = sp.sniff(iface=ifname, timeout=3) + found = False + for p in packets: + ip = p.getlayer(sp.IP) + if not ip: + continue + tcp = p.getlayer(sp.TCP) + if not tcp: + continue + + if ip.src != "192.0.2.2": + continue + + p.show() + + assert ip.dst == "192.0.2.3" + assert tcp.sport == 1234 + assert tcp.dport == 1234 + assert tcp.flags == "A" + + # We only expect one + assert not found + found = True + + assert found diff --git a/tests/sys/netpfil/pf/tcp.sh b/tests/sys/netpfil/pf/tcp.sh index 84536480b44e..f6a9ffde1383 100644 --- a/tests/sys/netpfil/pf/tcp.sh +++ b/tests/sys/netpfil/pf/tcp.sh @@ -33,12 +33,13 @@ rst_head() { atf_set descr 'Check sequence number validation in RST packets' atf_set require.user root - atf_set require.progs scapy + atf_set require.progs python3 scapy } rst_body() { pft_init + vnet_init_bridge epair_srv=$(vnet_mkepair) epair_cl=$(vnet_mkepair) diff --git a/tests/sys/netpfil/pf/utils.py b/tests/sys/netpfil/pf/utils.py new file mode 100644 index 000000000000..3d1c1de86aad --- /dev/null +++ b/tests/sys/netpfil/pf/utils.py @@ -0,0 +1,46 @@ +# +# SPDX-License-Identifier: BSD-2-Clause +# +# Copyright (c) 2025 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 threading +import time + +class DelayedSend(threading.Thread): + def __init__(self, packet, sendif=None): + threading.Thread.__init__(self) + self._packet = packet + self._sendif = sendif + + self.start() + + def run(self): + import scapy.all as sp + time.sleep(1) + + if self._sendif: + sp.sendp(self._packet, iface=self._sendif) + else: + sp.send(self._packet) + diff --git a/tests/sys/netpfil/pf/utils.subr b/tests/sys/netpfil/pf/utils.subr index 25720c1bcb66..6af10e80390d 100644 --- a/tests/sys/netpfil/pf/utils.subr +++ b/tests/sys/netpfil/pf/utils.subr @@ -83,6 +83,8 @@ pfsynct_init() pflog_init() { + pft_init + if ! kldstat -q -m pflog; then atf_skip "This test requires pflog" fi @@ -213,8 +215,9 @@ setup_router_server_ipv4() vnet_mkjail server ${epair_server}b jexec server ifconfig ${epair_server}b ${net_server_host_server}/${net_server_mask} up jexec server route add -net ${net_tester} ${net_server_host_router} - jexec server nc -4l 666 & - sleep 1 # Give nc time to start and listen + inetd_conf=$(mktemp) + echo "discard stream tcp nowait root internal" > $inetd_conf + jexec server inetd -p ${PWD}/inetd.pid $inetd_conf } # Create a bare router jail. @@ -266,8 +269,9 @@ setup_router_server_ipv6() vnet_mkjail server ${epair_server}b jexec server ifconfig ${epair_server}b inet6 ${net_server_host_server}/${net_server_mask} up no_dad jexec server route add -6 ${net_tester} ${net_server_host_router} - jexec server nc -6l 666 & - sleep 1 # Give nc time to start and listen + inetd_conf=$(mktemp) + echo "discard stream tcp6 nowait root internal" > $inetd_conf + jexec server inetd -p ${PWD}/inetd.pid $inetd_conf } # Ping the dummy static NDP target. @@ -297,3 +301,13 @@ ping_server_check_reply() --replyif ${epair_tester}a \ $params } + +normalize_pfctl_s() +{ + # `pfctl -s[rsS]` output is divided into sections. Each rule, state or + # source node starts with the beginning of a line and next lines with leading + # spaces are various parameters of said rule, state or source node. + # Convert it into a single line per entry, and remove multiple spaces, + # so that regular expressions for matching them in tests can be simpler. + awk '{ if ($0 ~ /^[^ ]/ && NR > 1) print(""); gsub(/ +/, " ", $0); printf("%s", $0); } END {print("");}' +} diff --git a/tests/sys/opencrypto/Makefile b/tests/sys/opencrypto/Makefile index 8be069349aa2..a9c06ef2a93b 100644 --- a/tests/sys/opencrypto/Makefile +++ b/tests/sys/opencrypto/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/opencrypto diff --git a/tests/sys/pjdfstest/Makefile b/tests/sys/pjdfstest/Makefile index 9ad68c2ac6f3..f8a6f1cd6b8d 100644 --- a/tests/sys/pjdfstest/Makefile +++ b/tests/sys/pjdfstest/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests SUBDIR+= pjdfstest diff --git a/tests/sys/pjdfstest/Makefile.inc b/tests/sys/pjdfstest/Makefile.inc index 03efce15f856..cec69b26e149 100644 --- a/tests/sys/pjdfstest/Makefile.inc +++ b/tests/sys/pjdfstest/Makefile.inc @@ -1,2 +1 @@ - .include "${SRCTOP}/tests/Makefile.inc0" diff --git a/tests/sys/pjdfstest/pjdfstest/Makefile b/tests/sys/pjdfstest/pjdfstest/Makefile index 30c4713da177..95e6d953d146 100644 --- a/tests/sys/pjdfstest/pjdfstest/Makefile +++ b/tests/sys/pjdfstest/pjdfstest/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests .PATH: ${SRCTOP}/contrib/pjdfstest diff --git a/tests/sys/pjdfstest/tests/Makefile b/tests/sys/pjdfstest/tests/Makefile index c2b5c3d166d5..abe854eb8c35 100644 --- a/tests/sys/pjdfstest/tests/Makefile +++ b/tests/sys/pjdfstest/tests/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests PJDFSTEST_SRCDIR= ${SRCTOP}/contrib/pjdfstest diff --git a/tests/sys/pjdfstest/tests/Makefile.inc b/tests/sys/pjdfstest/tests/Makefile.inc index d3b5cbd3a79b..01b5f23410c8 100644 --- a/tests/sys/pjdfstest/tests/Makefile.inc +++ b/tests/sys/pjdfstest/tests/Makefile.inc @@ -1,2 +1 @@ - .include "../Makefile.inc" diff --git a/tests/sys/pjdfstest/tests/chflags/Makefile b/tests/sys/pjdfstest/tests/chflags/Makefile index 308e86e9f533..e0dd497b6807 100644 --- a/tests/sys/pjdfstest/tests/chflags/Makefile +++ b/tests/sys/pjdfstest/tests/chflags/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/chmod/Makefile b/tests/sys/pjdfstest/tests/chmod/Makefile index a3e600f4d232..441bec65a357 100644 --- a/tests/sys/pjdfstest/tests/chmod/Makefile +++ b/tests/sys/pjdfstest/tests/chmod/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/chown/Makefile b/tests/sys/pjdfstest/tests/chown/Makefile index 1012341c82a4..8138a674f052 100644 --- a/tests/sys/pjdfstest/tests/chown/Makefile +++ b/tests/sys/pjdfstest/tests/chown/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/ftruncate/Makefile b/tests/sys/pjdfstest/tests/ftruncate/Makefile index 2f3425423764..30aeb30cb525 100644 --- a/tests/sys/pjdfstest/tests/ftruncate/Makefile +++ b/tests/sys/pjdfstest/tests/ftruncate/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/granular/Makefile b/tests/sys/pjdfstest/tests/granular/Makefile index a67a50ede7b2..28ff001d6958 100644 --- a/tests/sys/pjdfstest/tests/granular/Makefile +++ b/tests/sys/pjdfstest/tests/granular/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/link/Makefile b/tests/sys/pjdfstest/tests/link/Makefile index 7a890e96aef4..a94c3281a01f 100644 --- a/tests/sys/pjdfstest/tests/link/Makefile +++ b/tests/sys/pjdfstest/tests/link/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/mkdir/Makefile b/tests/sys/pjdfstest/tests/mkdir/Makefile index a3e600f4d232..441bec65a357 100644 --- a/tests/sys/pjdfstest/tests/mkdir/Makefile +++ b/tests/sys/pjdfstest/tests/mkdir/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/mkfifo/Makefile b/tests/sys/pjdfstest/tests/mkfifo/Makefile index a3e600f4d232..441bec65a357 100644 --- a/tests/sys/pjdfstest/tests/mkfifo/Makefile +++ b/tests/sys/pjdfstest/tests/mkfifo/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/mknod/Makefile b/tests/sys/pjdfstest/tests/mknod/Makefile index edfa0847268a..03f178034e85 100644 --- a/tests/sys/pjdfstest/tests/mknod/Makefile +++ b/tests/sys/pjdfstest/tests/mknod/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/open/Makefile b/tests/sys/pjdfstest/tests/open/Makefile index e4c656242d0a..12f54ba412d9 100644 --- a/tests/sys/pjdfstest/tests/open/Makefile +++ b/tests/sys/pjdfstest/tests/open/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/rename/Makefile b/tests/sys/pjdfstest/tests/rename/Makefile index 913c0d5d2a49..b6a4c06c28a4 100644 --- a/tests/sys/pjdfstest/tests/rename/Makefile +++ b/tests/sys/pjdfstest/tests/rename/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/rmdir/Makefile b/tests/sys/pjdfstest/tests/rmdir/Makefile index 19ecd8b0743a..974f7f741825 100644 --- a/tests/sys/pjdfstest/tests/rmdir/Makefile +++ b/tests/sys/pjdfstest/tests/rmdir/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/symlink/Makefile b/tests/sys/pjdfstest/tests/symlink/Makefile index a3e600f4d232..441bec65a357 100644 --- a/tests/sys/pjdfstest/tests/symlink/Makefile +++ b/tests/sys/pjdfstest/tests/symlink/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/truncate/Makefile b/tests/sys/pjdfstest/tests/truncate/Makefile index 2f3425423764..30aeb30cb525 100644 --- a/tests/sys/pjdfstest/tests/truncate/Makefile +++ b/tests/sys/pjdfstest/tests/truncate/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/unlink/Makefile b/tests/sys/pjdfstest/tests/unlink/Makefile index 308e86e9f533..e0dd497b6807 100644 --- a/tests/sys/pjdfstest/tests/unlink/Makefile +++ b/tests/sys/pjdfstest/tests/unlink/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/pjdfstest/tests/utimensat/Makefile b/tests/sys/pjdfstest/tests/utimensat/Makefile index 7dd5b6a02b20..16b90be6d6d8 100644 --- a/tests/sys/pjdfstest/tests/utimensat/Makefile +++ b/tests/sys/pjdfstest/tests/utimensat/Makefile @@ -1,4 +1,3 @@ - TAP_TESTS_SH= 00 TAP_TESTS_SH+= 01 TAP_TESTS_SH+= 02 diff --git a/tests/sys/posixshm/Makefile b/tests/sys/posixshm/Makefile index d7b46cd13867..9b87860fba58 100644 --- a/tests/sys/posixshm/Makefile +++ b/tests/sys/posixshm/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/posixshm diff --git a/tests/sys/posixshm/posixshm_test.c b/tests/sys/posixshm/posixshm_test.c index ade07a118707..55514a5f4bde 100644 --- a/tests/sys/posixshm/posixshm_test.c +++ b/tests/sys/posixshm/posixshm_test.c @@ -1190,6 +1190,33 @@ ATF_TC_BODY(accounting, tc) ATF_REQUIRE(close(fd) == 0); } +ATF_TC_WITHOUT_HEAD(mmap_prot); +ATF_TC_BODY(mmap_prot, tc) +{ + void *p; + int fd, pagesize; + + ATF_REQUIRE((pagesize = getpagesize()) > 0); + + gen_test_path(); + fd = shm_open(test_path, O_RDONLY | O_CREAT, 0644); + ATF_REQUIRE(fd >= 0); + + p = mmap(NULL, pagesize, PROT_READ, MAP_SHARED, fd, 0); + ATF_REQUIRE(p != MAP_FAILED); + ATF_REQUIRE(munmap(p, pagesize) == 0); + p = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + ATF_REQUIRE_ERRNO(EACCES, p == MAP_FAILED); + p = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0); + ATF_REQUIRE(p != MAP_FAILED); + ATF_REQUIRE(munmap(p, pagesize) == 0); + + ATF_REQUIRE_MSG(shm_unlink(test_path) == 0, + "shm_unlink failed; errno=%d", errno); + ATF_REQUIRE_MSG(close(fd) == 0, + "close failed; errno=%d", errno); +} + static int shm_open_large(int psind, int policy, size_t sz) { @@ -1920,7 +1947,6 @@ ATF_TC_BODY(largepage_reopen, tc) ATF_TP_ADD_TCS(tp) { - ATF_TP_ADD_TC(tp, remap_object); ATF_TP_ADD_TC(tp, rename_from_anon); ATF_TP_ADD_TC(tp, rename_bad_path_pointer); @@ -1954,6 +1980,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, fallocate); ATF_TP_ADD_TC(tp, fspacectl); ATF_TP_ADD_TC(tp, accounting); + ATF_TP_ADD_TC(tp, mmap_prot); ATF_TP_ADD_TC(tp, largepage_basic); ATF_TP_ADD_TC(tp, largepage_config); ATF_TP_ADD_TC(tp, largepage_mmap); diff --git a/tests/sys/sound/Makefile b/tests/sys/sound/Makefile new file mode 100644 index 000000000000..74a0765a0540 --- /dev/null +++ b/tests/sys/sound/Makefile @@ -0,0 +1,13 @@ +PACKAGE= tests + +TESTSDIR= ${TESTSBASE}/sys/sound + +ATF_TESTS_C+= pcm_read_write +ATF_TESTS_C+= sndstat + +CFLAGS+= -I${SRCTOP}/sys +LDFLAGS+= -lnv + +CFLAGS.pcm_read_write.c+= -Wno-cast-align + +.include <bsd.test.mk> diff --git a/tests/sys/sound/pcm_read_write.c b/tests/sys/sound/pcm_read_write.c new file mode 100644 index 000000000000..a77b953a78a0 --- /dev/null +++ b/tests/sys/sound/pcm_read_write.c @@ -0,0 +1,262 @@ +/*- + * Copyright (c) 2025 Florian Walpen + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * These tests exercise conversion functions of the sound module, used to read + * pcm samples from a buffer, and write pcm samples to a buffer. The test cases + * are non-exhaustive, but should detect systematic errors in conversion of the + * various sample formats supported. In particular, the test cases establish + * correctness independent of the machine's endianness, making them suitable to + * check for architecture-specific problems. + */ + +#include <sys/types.h> +#include <sys/soundcard.h> + +#include <atf-c.h> +#include <stdio.h> +#include <string.h> + +#include <dev/sound/pcm/sound.h> +#include <dev/sound/pcm/pcm.h> +#include <dev/sound/pcm/g711.h> + +/* Generic test data, with buffer content matching the sample values. */ +static struct afmt_test_data { + const char *label; + uint8_t buffer[4]; + size_t size; + int format; + intpcm_t value; + _Static_assert((sizeof(intpcm_t) == 4), + "Test data assumes 32bit, adjust negative values to new size."); +} const afmt_tests[] = { + /* 8 bit sample formats. */ + {"s8_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_S8, 0x00000001}, + {"s8_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_S8, 0xffffff81}, + {"u8_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_U8, 0xffffff81}, + {"u8_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_U8, 0x00000001}, + + /* 16 bit sample formats. */ + {"s16le_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_S16_LE, 0x00000201}, + {"s16le_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_S16_LE, 0xffff8281}, + {"s16be_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_S16_BE, 0x00000102}, + {"s16be_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_S16_BE, 0xffff8182}, + {"u16le_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_U16_LE, 0xffff8201}, + {"u16le_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_U16_LE, 0x00000281}, + {"u16be_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_U16_BE, 0xffff8102}, + {"u16be_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_U16_BE, 0x00000182}, + + /* 24 bit sample formats. */ + {"s24le_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_S24_LE, 0x00030201}, + {"s24le_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_S24_LE, 0xff838281}, + {"s24be_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_S24_BE, 0x00010203}, + {"s24be_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_S24_BE, 0xff818283}, + {"u24le_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_U24_LE, 0xff830201}, + {"u24le_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_U24_LE, 0x00038281}, + {"u24be_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_U24_BE, 0xff810203}, + {"u24be_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_U24_BE, 0x00018283}, + + /* 32 bit sample formats. */ + {"s32le_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_S32_LE, 0x04030201}, + {"s32le_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_S32_LE, 0x84838281}, + {"s32be_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_S32_BE, 0x01020304}, + {"s32be_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_S32_BE, 0x81828384}, + {"u32le_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_U32_LE, 0x84030201}, + {"u32le_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_U32_LE, 0x04838281}, + {"u32be_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_U32_BE, 0x81020304}, + {"u32be_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_U32_BE, 0x01828384}, + + /* 32 bit floating point sample formats. */ + {"f32le_1", {0x00, 0x00, 0x00, 0x3f}, 4, AFMT_F32_LE, 0x40000000}, + {"f32le_2", {0x00, 0x00, 0x00, 0xbf}, 4, AFMT_F32_LE, 0xc0000000}, + {"f32be_1", {0x3f, 0x00, 0x00, 0x00}, 4, AFMT_F32_BE, 0x40000000}, + {"f32be_2", {0xbf, 0x00, 0x00, 0x00}, 4, AFMT_F32_BE, 0xc0000000}, + + /* u-law and A-law sample formats. */ + {"mulaw_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_MU_LAW, 0xffffff87}, + {"mulaw_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_MU_LAW, 0x00000079}, + {"alaw_1", {0x2a, 0x00, 0x00, 0x00}, 1, AFMT_A_LAW, 0xffffff83}, + {"alaw_2", {0xab, 0x00, 0x00, 0x00}, 1, AFMT_A_LAW, 0x00000079} +}; + +/* Normalize sample values in strictly correct (but slow) c. */ +static intpcm_t +local_normalize(intpcm_t value, int val_bits, int norm_bits) +{ + int32_t divisor; + intpcm_t remainder; + + /* Avoid undefined or implementation defined behavior. */ + if (val_bits < norm_bits) + /* Multiply instead of left shift (value may be negative). */ + return (value * (1 << (norm_bits - val_bits))); + else if (val_bits > norm_bits) { + divisor = (1 << (val_bits - norm_bits)); + /* Positive remainder, to discard lowest bits from value. */ + remainder = value % divisor; + remainder = (remainder + divisor) % divisor; + /* Divide instead of right shift (value may be negative). */ + return ((value - remainder) / divisor); + } + return value; +} + +/* Restrict magnitude of sample value to 24bit for 32bit calculations. */ +static intpcm_t +local_calc_limit(intpcm_t value, int val_bits) +{ + /* + * When intpcm32_t is defined to be 32bit, calculations for mixing and + * volume changes use 32bit integers instead of 64bit. To get some + * headroom for calculations, 32bit sample values are restricted to + * 24bit magnitude in that case. Also avoid implementation defined + * behavior here. + */ + if (sizeof(intpcm32_t) == (32 / 8) && val_bits == 32) + return (local_normalize(value, 32, 24)); + return value; +} + +ATF_TC(pcm_read); +ATF_TC_HEAD(pcm_read, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Read and verify different pcm sample formats."); +} +ATF_TC_BODY(pcm_read, tc) +{ + const struct afmt_test_data *test; + uint8_t src[4]; + intpcm_t expected, result; + size_t i; + + for (i = 0; i < nitems(afmt_tests); i++) { + test = &afmt_tests[i]; + + /* Copy byte representation, fill with distinctive pattern. */ + memset(src, 0x66, sizeof(src)); + memcpy(src, test->buffer, test->size); + + /* Read sample at format magnitude. */ + expected = test->value; + result = pcm_sample_read(src, test->format); + ATF_CHECK_MSG(result == expected, + "pcm_read[\"%s\"].value: expected=0x%08x, result=0x%08x", + test->label, expected, result); + + /* Read sample at format magnitude, for calculations. */ + expected = local_calc_limit(test->value, test->size * 8); + result = pcm_sample_read_calc(src, test->format); + ATF_CHECK_MSG(result == expected, + "pcm_read[\"%s\"].calc: expected=0x%08x, result=0x%08x", + test->label, expected, result); + + /* Read sample at full 32 bit magnitude. */ + expected = local_normalize(test->value, test->size * 8, 32); + result = pcm_sample_read_norm(src, test->format); + ATF_CHECK_MSG(result == expected, + "pcm_read[\"%s\"].norm: expected=0x%08x, result=0x%08x", + test->label, expected, result); + } +} + +ATF_TC(pcm_write); +ATF_TC_HEAD(pcm_write, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Write and verify different pcm sample formats."); +} +ATF_TC_BODY(pcm_write, tc) +{ + const struct afmt_test_data *test; + uint8_t expected[4]; + uint8_t dst[4]; + intpcm_t value; + size_t i; + + for (i = 0; i < nitems(afmt_tests); i++) { + test = &afmt_tests[i]; + + /* Write sample of format specific magnitude. */ + memcpy(expected, test->buffer, sizeof(expected)); + memset(dst, 0x00, sizeof(dst)); + value = test->value; + pcm_sample_write(dst, value, test->format); + ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0, + "pcm_write[\"%s\"].value: " + "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, " + "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label, + expected[0], expected[1], expected[2], expected[3], + dst[0], dst[1], dst[2], dst[3]); + + /* Write sample of format specific, calculation magnitude. */ + memcpy(expected, test->buffer, sizeof(expected)); + memset(dst, 0x00, sizeof(dst)); + value = local_calc_limit(test->value, test->size * 8); + if (value != test->value) { + /* + * 32 bit sample was reduced to 24 bit resolution + * for calculation, least significant byte is lost. + */ + if (test->format & AFMT_BIGENDIAN) + expected[3] = 0x00; + else + expected[0] = 0x00; + } + pcm_sample_write_calc(dst, value, test->format); + ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0, + "pcm_write[\"%s\"].calc: " + "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, " + "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label, + expected[0], expected[1], expected[2], expected[3], + dst[0], dst[1], dst[2], dst[3]); + + /* Write normalized sample of full 32 bit magnitude. */ + memcpy(expected, test->buffer, sizeof(expected)); + memset(dst, 0x00, sizeof(dst)); + value = local_normalize(test->value, test->size * 8, 32); + pcm_sample_write_norm(dst, value, test->format); + ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0, + "pcm_write[\"%s\"].norm: " + "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, " + "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label, + expected[0], expected[1], expected[2], expected[3], + dst[0], dst[1], dst[2], dst[3]); + } +} + +ATF_TC(pcm_format_bits); +ATF_TC_HEAD(pcm_format_bits, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Verify bit width of different pcm sample formats."); +} +ATF_TC_BODY(pcm_format_bits, tc) +{ + const struct afmt_test_data *test; + size_t bits; + size_t i; + + for (i = 0; i < nitems(afmt_tests); i++) { + test = &afmt_tests[i]; + + /* Check bit width determined for given sample format. */ + bits = AFMT_BIT(test->format); + ATF_CHECK_MSG(bits == test->size * 8, + "format_bits[%zu].size: expected=%zu, result=%zu", + i, test->size * 8, bits); + } +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, pcm_read); + ATF_TP_ADD_TC(tp, pcm_write); + ATF_TP_ADD_TC(tp, pcm_format_bits); + + return atf_no_error(); +} diff --git a/tests/sys/sound/sndstat.c b/tests/sys/sound/sndstat.c new file mode 100644 index 000000000000..ed292b570429 --- /dev/null +++ b/tests/sys/sound/sndstat.c @@ -0,0 +1,395 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 The FreeBSD Foundation + * + * This software was developed by Christos Margiolis <christos@FreeBSD.org> + * under sponsorship from the FreeBSD Foundation. + * + * 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. + */ + +#include <sys/param.h> +#include <sys/linker.h> +#include <sys/nv.h> +#include <sys/sndstat.h> +#include <sys/soundcard.h> + +#include <atf-c.h> +#include <errno.h> +#include <fcntl.h> +#include <stdlib.h> +#include <unistd.h> + +static void +load_dummy(void) +{ + if (kldload("snd_dummy.ko") < 0 && errno != EEXIST) + atf_tc_skip("snd_dummy.ko not found"); +} + +ATF_TC(sndstat_nv); +ATF_TC_HEAD(sndstat_nv, tc) +{ + atf_tc_set_md_var(tc, "descr", "/dev/sndstat nvlist test"); +} + +ATF_TC_BODY(sndstat_nv, tc) +{ + nvlist_t *nvl; + const nvlist_t * const *di; + const nvlist_t * const *cdi; + struct sndstioc_nv_arg arg; + size_t nitems, nchans, i, j; + int fd, rc, pchan, rchan; + + load_dummy(); + + if ((fd = open("/dev/sndstat", O_RDONLY)) < 0) + atf_tc_skip("/dev/sndstat not found, load sound(4)"); + + rc = ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL); + ATF_REQUIRE_EQ(rc, 0); + + arg.nbytes = 0; + arg.buf = NULL; + rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg); + ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#1) failed"); + + arg.buf = malloc(arg.nbytes); + ATF_REQUIRE(arg.buf != NULL); + + rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg); + ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#2) failed"); + + nvl = nvlist_unpack(arg.buf, arg.nbytes, 0); + ATF_REQUIRE(nvl != NULL); + + if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS)) + atf_tc_skip("no soundcards attached"); + + di = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &nitems); + for (i = 0; i < nitems; i++) { +#define NV(type, item) do { \ + ATF_REQUIRE_MSG(nvlist_exists(di[i], SNDST_DSPS_ ## item), \ + "SNDST_DSPS_" #item " does not exist"); \ + nvlist_get_ ## type (di[i], SNDST_DSPS_ ## item); \ +} while (0) + NV(string, NAMEUNIT); + NV(bool, FROM_USER); + NV(string, DEVNODE); + NV(string, DESC); + NV(string, PROVIDER); + NV(number, PCHAN); + NV(number, RCHAN); +#undef NV + + /* Cannot asign using the macro. */ + pchan = nvlist_get_number(di[i], SNDST_DSPS_PCHAN); + rchan = nvlist_get_number(di[i], SNDST_DSPS_RCHAN); + + if (pchan && !nvlist_exists(di[i], SNDST_DSPS_INFO_PLAY)) + atf_tc_fail("playback channel list empty"); + if (rchan && !nvlist_exists(di[i], SNDST_DSPS_INFO_REC)) + atf_tc_fail("recording channel list empty"); + +#define NV(type, mode, item) do { \ + ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(di[i], \ + SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item), \ + "SNDST_DSPS_INFO_" #item " does not exist"); \ + nvlist_get_ ## type (nvlist_get_nvlist(di[i], \ + SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item); \ +} while (0) + if (pchan) { + NV(number, PLAY, MIN_RATE); + NV(number, PLAY, MAX_RATE); + NV(number, PLAY, FORMATS); + NV(number, PLAY, MIN_CHN); + NV(number, PLAY, MAX_CHN); + } + if (rchan) { + NV(number, REC, MIN_RATE); + NV(number, REC, MAX_RATE); + NV(number, REC, FORMATS); + NV(number, REC, MIN_CHN); + NV(number, REC, MAX_CHN); + } +#undef NV + + if (!nvlist_exists(di[i], SNDST_DSPS_PROVIDER_INFO)) + continue; + +#define NV(type, item) do { \ + ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(di[i], \ + SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_ ## item), \ + "SNDST_DSPS_SOUND4_" #item " does not exist"); \ + nvlist_get_ ## type (nvlist_get_nvlist(di[i], \ + SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_ ## item); \ +} while (0) + NV(number, UNIT); + NV(string, STATUS); + NV(bool, BITPERFECT); + NV(bool, PVCHAN); + NV(number, PVCHANRATE); + NV(number, PVCHANFORMAT); + NV(bool, RVCHAN); + NV(number, PVCHANRATE); + NV(number, PVCHANFORMAT); +#undef NV + + if (!nvlist_exists(nvlist_get_nvlist(di[i], + SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_CHAN_INFO)) + atf_tc_fail("channel info list empty"); + + cdi = nvlist_get_nvlist_array( + nvlist_get_nvlist(di[i], SNDST_DSPS_PROVIDER_INFO), + SNDST_DSPS_SOUND4_CHAN_INFO, &nchans); + for (j = 0; j < nchans; j++) { +#define NV(type, item) do { \ + ATF_REQUIRE_MSG(nvlist_exists(cdi[j], SNDST_DSPS_SOUND4_CHAN_ ## item), \ + "SNDST_DSPS_SOUND4_CHAN_" #item " does not exist"); \ + nvlist_get_ ## type (cdi[j], SNDST_DSPS_SOUND4_CHAN_ ## item); \ +} while (0) + NV(string, NAME); + NV(string, PARENTCHAN); + NV(number, UNIT); + NV(number, CAPS); + NV(number, LATENCY); + NV(number, RATE); + NV(number, FORMAT); + NV(number, PID); + NV(string, COMM); + NV(number, INTR); + NV(number, XRUNS); + NV(number, FEEDCNT); + NV(number, LEFTVOL); + NV(number, RIGHTVOL); + NV(number, HWBUF_FORMAT); + NV(number, HWBUF_RATE); + NV(number, HWBUF_SIZE); + NV(number, HWBUF_BLKSZ); + NV(number, HWBUF_BLKCNT); + NV(number, HWBUF_FREE); + NV(number, HWBUF_READY); + NV(number, SWBUF_FORMAT); + NV(number, SWBUF_RATE); + NV(number, SWBUF_SIZE); + NV(number, SWBUF_BLKSZ); + NV(number, SWBUF_BLKCNT); + NV(number, SWBUF_FREE); + NV(number, SWBUF_READY); + NV(string, FEEDERCHAIN); +#undef NV + } + } + + free(arg.buf); + nvlist_destroy(nvl); + close(fd); +} + +#define UDEV_PROVIDER "sndstat_udev" +#define UDEV_NAMEUNIT "sndstat_udev" +#define UDEV_DEVNODE "sndstat_udev" +#define UDEV_DESC "Test Device" +#define UDEV_PCHAN 1 +#define UDEV_RCHAN 1 +#define UDEV_MIN_RATE 8000 +#define UDEV_MAX_RATE 96000 +#define UDEV_FORMATS (AFMT_S16_NE | AFMT_S24_NE | AFMT_S32_NE) +#define UDEV_MIN_CHN 1 +#define UDEV_MAX_CHN 2 + +ATF_TC(sndstat_udev); +ATF_TC_HEAD(sndstat_udev, tc) +{ + atf_tc_set_md_var(tc, "descr", "/dev/sndstat userdev interface test"); +} + +ATF_TC_BODY(sndstat_udev, tc) +{ + nvlist_t *nvl, *di, *dichild; + const nvlist_t * const *rdi; + struct sndstioc_nv_arg arg; + const char *str; + size_t nitems, i; + int fd, rc, pchan, rchan, n; + + load_dummy(); + + if ((fd = open("/dev/sndstat", O_RDWR)) < 0) + atf_tc_skip("/dev/sndstat not found, load sound(4)"); + + nvl = nvlist_create(0); + ATF_REQUIRE(nvl != NULL); + + di = nvlist_create(0); + ATF_REQUIRE(di != NULL); + + dichild = nvlist_create(0); + ATF_REQUIRE(dichild != NULL); + + nvlist_add_string(di, SNDST_DSPS_PROVIDER, UDEV_PROVIDER); + nvlist_add_string(di, SNDST_DSPS_NAMEUNIT, UDEV_NAMEUNIT); + nvlist_add_string(di, SNDST_DSPS_DESC, UDEV_DESC); + nvlist_add_string(di, SNDST_DSPS_DEVNODE, UDEV_DEVNODE); + nvlist_add_number(di, SNDST_DSPS_PCHAN, UDEV_PCHAN); + nvlist_add_number(di, SNDST_DSPS_RCHAN, UDEV_RCHAN); + + nvlist_add_number(dichild, SNDST_DSPS_INFO_MIN_RATE, UDEV_MIN_RATE); + nvlist_add_number(dichild, SNDST_DSPS_INFO_MAX_RATE, UDEV_MAX_RATE); + nvlist_add_number(dichild, SNDST_DSPS_INFO_FORMATS, UDEV_FORMATS); + nvlist_add_number(dichild, SNDST_DSPS_INFO_MIN_CHN, UDEV_MIN_CHN); + nvlist_add_number(dichild, SNDST_DSPS_INFO_MAX_CHN, UDEV_MAX_CHN); + + nvlist_add_nvlist(di, SNDST_DSPS_INFO_PLAY, dichild); + nvlist_add_nvlist(di, SNDST_DSPS_INFO_REC, dichild); + + nvlist_append_nvlist_array(nvl, SNDST_DSPS, di); + ATF_REQUIRE_EQ(nvlist_error(nvl), 0); + + arg.buf = nvlist_pack(nvl, &arg.nbytes); + ATF_REQUIRE_MSG(arg.buf != NULL, "failed to pack nvlist"); + + rc = ioctl(fd, SNDSTIOC_ADD_USER_DEVS, &arg); + free(arg.buf); + ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_ADD_USER_DEVS) failed"); + + nvlist_destroy(di); + nvlist_destroy(dichild); + nvlist_destroy(nvl); + + /* Read back registered values. */ + rc = ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL); + ATF_REQUIRE_EQ(rc, 0); + + arg.nbytes = 0; + arg.buf = NULL; + rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg); + ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#1) failed"); + + arg.buf = malloc(arg.nbytes); + ATF_REQUIRE(arg.buf != NULL); + + rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg); + ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#2) failed"); + + nvl = nvlist_unpack(arg.buf, arg.nbytes, 0); + ATF_REQUIRE(nvl != NULL); + + if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS)) + atf_tc_skip("no soundcards attached"); + + rdi = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &nitems); + for (i = 0; i < nitems; i++) { +#define NV(type, item, var) do { \ + ATF_REQUIRE_MSG(nvlist_exists(rdi[i], SNDST_DSPS_ ## item), \ + "SNDST_DSPS_" #item " does not exist"); \ + var = nvlist_get_ ## type (rdi[i], SNDST_DSPS_ ## item); \ +} while (0) + /* Search for our device. */ + NV(string, NAMEUNIT, str); + if (strcmp(str, UDEV_NAMEUNIT) == 0) + break; + } + if (i == nitems) + atf_tc_fail("userland device %s not found", UDEV_NAMEUNIT); + + NV(string, NAMEUNIT, str); + ATF_CHECK(strcmp(str, UDEV_NAMEUNIT) == 0); + + NV(bool, FROM_USER, n); + ATF_CHECK(n); + + NV(string, DEVNODE, str); + ATF_CHECK(strcmp(str, UDEV_DEVNODE) == 0); + + NV(string, DESC, str); + ATF_CHECK(strcmp(str, UDEV_DESC) == 0); + + NV(string, PROVIDER, str); + ATF_CHECK(strcmp(str, UDEV_PROVIDER) == 0); + + NV(number, PCHAN, pchan); + ATF_CHECK(pchan == UDEV_PCHAN); + if (pchan && !nvlist_exists(rdi[i], SNDST_DSPS_INFO_PLAY)) + atf_tc_fail("playback channel list empty"); + + NV(number, RCHAN, rchan); + ATF_CHECK(rchan == UDEV_RCHAN); + if (rchan && !nvlist_exists(rdi[i], SNDST_DSPS_INFO_REC)) + atf_tc_fail("recording channel list empty"); +#undef NV + +#define NV(type, mode, item, var) do { \ + ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(rdi[i], \ + SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item), \ + "SNDST_DSPS_INFO_" #item " does not exist"); \ + var = nvlist_get_ ## type (nvlist_get_nvlist(rdi[i], \ + SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item); \ +} while (0) + if (pchan) { + NV(number, PLAY, MIN_RATE, n); + ATF_CHECK(n == UDEV_MIN_RATE); + + NV(number, PLAY, MAX_RATE, n); + ATF_CHECK(n == UDEV_MAX_RATE); + + NV(number, PLAY, FORMATS, n); + ATF_CHECK(n == UDEV_FORMATS); + + NV(number, PLAY, MIN_CHN, n); + ATF_CHECK(n == UDEV_MIN_CHN); + + NV(number, PLAY, MAX_CHN, n); + ATF_CHECK(n == UDEV_MAX_CHN); + } + if (rchan) { + NV(number, REC, MIN_RATE, n); + ATF_CHECK(n == UDEV_MIN_RATE); + + NV(number, REC, MAX_RATE, n); + ATF_CHECK(n == UDEV_MAX_RATE); + + NV(number, REC, FORMATS, n); + ATF_CHECK(n == UDEV_FORMATS); + + NV(number, REC, MIN_CHN, n); + ATF_CHECK(n == UDEV_MIN_CHN); + + NV(number, REC, MAX_CHN, n); + ATF_CHECK(n == UDEV_MAX_CHN); + } +#undef NV + + free(arg.buf); + nvlist_destroy(nvl); + close(fd); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, sndstat_nv); + ATF_TP_ADD_TC(tp, sndstat_udev); + + return (atf_no_error()); +} diff --git a/tests/sys/sys/Makefile b/tests/sys/sys/Makefile index e9c9bfef7dbd..a1b4e3234e1c 100644 --- a/tests/sys/sys/Makefile +++ b/tests/sys/sys/Makefile @@ -1,4 +1,3 @@ - .include <bsd.compiler.mk> TESTSDIR= ${TESTSBASE}/sys/sys @@ -6,7 +5,9 @@ TESTSDIR= ${TESTSBASE}/sys/sys ATF_TESTS_C= arb_test \ bitset_test \ bitstring_test \ + buf_ring_test \ qmath_test \ + queue_test \ rb_test \ splay_test \ time_test diff --git a/tests/sys/sys/buf_ring_test.c b/tests/sys/sys/buf_ring_test.c new file mode 100644 index 000000000000..611fb23788cc --- /dev/null +++ b/tests/sys/sys/buf_ring_test.c @@ -0,0 +1,180 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2024 Arm Ltd + */ + +#include <sys/types.h> + +#include <machine/cpu.h> +#include <machine/cpufunc.h> + +#include <errno.h> +#include <stdint.h> + +#include <atf-c.h> + +static void critical_enter(void); +static void critical_exit(void); + +#include <sys/buf_ring.h> + +static void +critical_enter(void) +{ +} + +static void +critical_exit(void) +{ +} + +static void * +buf_ring_dequeue_peek(struct buf_ring *br) +{ + void *val; + + val = buf_ring_peek(br); + if (val != NULL) + buf_ring_advance_sc(br); + return (val); +} + +static void * +buf_ring_dequeue_peek_clear_sc(struct buf_ring *br) +{ + void *val; + + val = buf_ring_peek_clear_sc(br); + if (val != NULL) + buf_ring_advance_sc(br); + return (val); +} + +#define MC_SC_TEST(dequeue_func) \ +ATF_TC_WITHOUT_HEAD(dequeue_func); \ +ATF_TC_BODY(dequeue_func, tc) \ +{ \ + struct buf_ring *br; \ + \ + br = buf_ring_alloc(4); \ + ATF_REQUIRE_MSG(br != NULL, "buf_ring_alloc returned NULL"); \ + \ + ATF_REQUIRE(dequeue_func(br) == NULL); \ + ATF_REQUIRE(buf_ring_count(br) == 0); \ + ATF_REQUIRE(!buf_ring_full(br)); \ + ATF_REQUIRE(buf_ring_empty(br)); \ + \ + /* Try filling the buf_ring */ \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)1) == 0); \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)2) == 0); \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)3) == 0); \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)4) == ENOBUFS); \ + \ + ATF_REQUIRE(buf_ring_count(br) == 3); \ + ATF_REQUIRE(buf_ring_full(br)); \ + ATF_REQUIRE(!buf_ring_empty(br)); \ + \ + /* Partially empty it */ \ + ATF_REQUIRE(dequeue_func(br) == (void *)1); \ + ATF_REQUIRE(dequeue_func(br) == (void *)2); \ + \ + ATF_REQUIRE(buf_ring_count(br) == 1); \ + ATF_REQUIRE(!buf_ring_full(br)); \ + ATF_REQUIRE(!buf_ring_empty(br)); \ + \ + /* Add more items */ \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)5) == 0); \ + ATF_REQUIRE(buf_ring_count(br) == 2); \ + \ + /* Finish emptying it */ \ + ATF_REQUIRE(dequeue_func(br) == (void *)3); \ + ATF_REQUIRE(dequeue_func(br) == (void *)5); \ + ATF_REQUIRE(dequeue_func(br) == NULL); \ + \ + ATF_REQUIRE(buf_ring_count(br) == 0); \ + ATF_REQUIRE(!buf_ring_full(br)); \ + ATF_REQUIRE(buf_ring_empty(br)); \ + \ + for (uintptr_t i = 0; i < 8; i++) { \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 100)) == 0); \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 200)) == 0); \ + ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 300)) == 0); \ + ATF_REQUIRE(buf_ring_count(br) == 3); \ + ATF_REQUIRE(dequeue_func(br) == (void *)(i + 100)); \ + ATF_REQUIRE(dequeue_func(br) == (void *)(i + 200)); \ + ATF_REQUIRE(dequeue_func(br) == (void *)(i + 300)); \ + \ + ATF_REQUIRE(!buf_ring_full(br)); \ + ATF_REQUIRE(buf_ring_empty(br)); \ + } \ + \ + buf_ring_free(br); \ +} + +MC_SC_TEST(buf_ring_dequeue_sc) +MC_SC_TEST(buf_ring_dequeue_mc) +MC_SC_TEST(buf_ring_dequeue_peek) +MC_SC_TEST(buf_ring_dequeue_peek_clear_sc) + +ATF_TC_WITHOUT_HEAD(overflow); +ATF_TC_BODY(overflow, tc) +{ + struct buf_ring *br; + + br = buf_ring_alloc(4); + ATF_REQUIRE_MSG(br != NULL, "buf_ring_alloc returned NULL"); + + br->br_prod_head = br->br_cons_head = br->br_prod_tail = + br->br_cons_tail = UINT32_MAX - 1; + ATF_REQUIRE(buf_ring_count(br) == 0); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(buf_ring_empty(br)); + + ATF_REQUIRE(buf_ring_enqueue(br, (void *)1) == 0); + ATF_REQUIRE(buf_ring_count(br) == 1); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(!buf_ring_empty(br)); + + ATF_REQUIRE(buf_ring_enqueue(br, (void *)2) == 0); + ATF_REQUIRE(buf_ring_count(br) == 2); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(!buf_ring_empty(br)); + + ATF_REQUIRE(buf_ring_enqueue(br, (void *)3) == 0); + ATF_REQUIRE(buf_ring_count(br) == 3); + ATF_REQUIRE(buf_ring_full(br)); + ATF_REQUIRE(!buf_ring_empty(br)); + + ATF_REQUIRE(br->br_prod_head == 1); + ATF_REQUIRE(br->br_prod_tail == 1); + ATF_REQUIRE(br->br_cons_head == UINT32_MAX - 1); + ATF_REQUIRE(br->br_cons_tail == UINT32_MAX - 1); + + ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)1); + ATF_REQUIRE(buf_ring_count(br) == 2); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(!buf_ring_empty(br)); + + ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)2); + ATF_REQUIRE(buf_ring_count(br) == 1); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(!buf_ring_empty(br)); + + ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)3); + ATF_REQUIRE(buf_ring_count(br) == 0); + ATF_REQUIRE(!buf_ring_full(br)); + ATF_REQUIRE(buf_ring_empty(br)); + + buf_ring_free(br); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, buf_ring_dequeue_sc); + ATF_TP_ADD_TC(tp, buf_ring_dequeue_mc); + ATF_TP_ADD_TC(tp, buf_ring_dequeue_peek); + ATF_TP_ADD_TC(tp, buf_ring_dequeue_peek_clear_sc); + ATF_TP_ADD_TC(tp, overflow); + return (atf_no_error()); +} diff --git a/tests/sys/sys/queue_test.c b/tests/sys/sys/queue_test.c new file mode 100644 index 000000000000..7f8738751b85 --- /dev/null +++ b/tests/sys/sys/queue_test.c @@ -0,0 +1,293 @@ +/* + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2025 The FreeBSD Foundation + * + * This software was developed by Olivier Certner <olce@FreeBSD.org> at + * Kumacom SARL under sponsorship from the FreeBSD Foundation. + */ + +#include <sys/types.h> +#define QUEUE_MACRO_DEBUG_ASSERTIONS +#include <sys/queue.h> + +#include <stdio.h> +#include <stdlib.h> + +#include <atf-c.h> + +/* + * General utilities. + */ +#define DIAG(fmt, ...) do { \ + fprintf(stderr, "%s(): " fmt "\n", __func__, ##__VA_ARGS__); \ +} while (0) + +/* + * Common definitions and utilities. + * + * 'type' should be tailq, stailq, list or slist. 'TYPE' is 'type' in + * uppercase. + */ + +#define QUEUE_TESTS_COMMON(type, TYPE) \ +/* \ + * Definitions and utilities. \ + */ \ + \ +struct type ## _id_elem { \ + TYPE ## _ENTRY(type ## _id_elem) ie_entry; \ + u_int ie_id; \ +}; \ + \ +TYPE ## _HEAD(type ## _ids, type ## _id_elem); \ + \ +static void \ +type ## _check(const struct type ## _ids *const type, \ + const u_int nb, const u_int id_shift); \ + \ +/* \ + * Creates a tailq/list with 'nb' elements with contiguous IDs \ + * in ascending order starting at 'id_shift'. \ + */ \ +static struct type ## _ids * \ +type ## _create(const u_int nb, const u_int id_shift) \ +{ \ + struct type ## _ids *const type = \ + malloc(sizeof(*type)); \ + \ + ATF_REQUIRE_MSG(type != NULL, \ + "Cannot malloc " #type " head"); \ + \ + TYPE ## _INIT(type); \ + for (u_int i = 0; i < nb; ++i) { \ + struct type ## _id_elem *const e = \ + malloc(sizeof(*e)); \ + \ + ATF_REQUIRE_MSG(e != NULL, \ + "Cannot malloc " #type " element %u", i); \ + e->ie_id = nb - 1 - i + id_shift; \ + TYPE ## _INSERT_HEAD(type, e, ie_entry); \ + } \ + \ + DIAG("Created " #type " %p with %u elements", \ + type, nb); \ + type ## _check(type, nb, id_shift); \ + return (type); \ +} \ + \ +/* Performs no check. */ \ +static void \ +type ## _destroy(struct type ## _ids *const type) \ +{ \ + struct type ## _id_elem *e, *tmp_e; \ + \ + DIAG("Destroying " #type" %p", type); \ + TYPE ## _FOREACH_SAFE(e, type, ie_entry, \ + tmp_e) { \ + free(e); \ + } \ + free(type); \ +} \ + \ + \ +/* Checks that some tailq/list is as produced by *_create(). */ \ +static void \ +type ## _check(const struct type ## _ids *const type, \ + const u_int nb, const u_int id_shift) \ +{ \ + struct type ## _id_elem *e; \ + u_int i = 0; \ + \ + TYPE ## _FOREACH(e, type, ie_entry) { \ + ATF_REQUIRE_MSG(i + 1 <= nb, \ + #type " %p has more than %u elements", \ + type, nb); \ + ATF_REQUIRE_MSG(e->ie_id == i + id_shift, \ + #type " %p element %p: Found ID %u, " \ + "expected %u", \ + type, e, e->ie_id, i + id_shift); \ + ++i; \ + } \ + ATF_REQUIRE_MSG(i == nb, \ + #type " %p has only %u elements, expected %u", \ + type, i, nb); \ +} \ + \ +/* Returns NULL if not enough elements. */ \ +static struct type ## _id_elem * \ +type ## _nth(const struct type ## _ids *const type, \ + const u_int idx) \ +{ \ + struct type ## _id_elem *e; \ + u_int i = 0; \ + \ + TYPE ## _FOREACH(e, type, ie_entry) { \ + if (i == idx) { \ + DIAG(#type " %p has element %p " \ + "(ID %u) at index %u", \ + type, e, e->ie_id, idx); \ + return (e); \ + } \ + ++i; \ + } \ + DIAG(#type " %p: Only %u elements, no index %u", \ + type, i, idx); \ + return (NULL); \ +} \ + \ +/* \ + * Tests. \ + */ \ + \ +ATF_TC(type ## _split_after_and_concat); \ +ATF_TC_HEAD(type ## _split_after_and_concat, tc) \ +{ \ + atf_tc_set_md_var(tc, "descr", \ + "Test " #TYPE "_SPLIT_AFTER() followed by " \ + #TYPE "_CONCAT()"); \ +} \ +ATF_TC_BODY(type ## _split_after_and_concat, tc) \ +{ \ + struct type ## _ids *const type = \ + type ## _create(100, 0); \ + struct type ## _ids rest; \ + struct type ## _id_elem *e; \ + \ + e = type ## _nth(type, 49); \ + TYPE ## _SPLIT_AFTER(type, e, &rest, ie_entry); \ + type ## _check(type, 50, 0); \ + type ## _check(&rest, 50, 50); \ + QUEUE_TESTS_ ## TYPE ## _CONCAT(type, &rest); \ + ATF_REQUIRE_MSG(TYPE ## _EMPTY(&rest), \ + "'rest' not empty after concat"); \ + type ## _check(type, 100, 0); \ + type ## _destroy(type); \ +} + +#define QUEUE_TESTS_CHECK_REVERSED(type, TYPE) \ +/* \ + * Checks that some tailq/list is reversed. \ + */ \ +static void \ +type ## _check_reversed(const struct type ## _ids *const type, \ + const u_int nb, const u_int id_shift) \ +{ \ + struct type ## _id_elem *e; \ + u_int i = 0; \ + \ + TYPE ## _FOREACH(e, type, ie_entry) { \ + const u_int expected_id = nb - 1 - i + id_shift; \ + \ + ATF_REQUIRE_MSG(i < nb, \ + #type " %p has more than %u elements", \ + type, nb); \ + ATF_REQUIRE_MSG(e->ie_id == expected_id, \ + #type " %p element %p, idx %u: Found ID %u, " \ + "expected %u", \ + type, e, i, e->ie_id, expected_id); \ + ++i; \ + } \ + ATF_REQUIRE_MSG(i == nb, \ + #type " %p has only %u elements, expected %u", \ + type, i, nb); \ +} + +/* + * Paper over the *_CONCAT() signature differences. + */ + +#define QUEUE_TESTS_TAILQ_CONCAT(first, second) \ + TAILQ_CONCAT(first, second, ie_entry) + +#define QUEUE_TESTS_LIST_CONCAT(first, second) \ + LIST_CONCAT(first, second, list_id_elem, ie_entry) + +#define QUEUE_TESTS_STAILQ_CONCAT(first, second) \ + STAILQ_CONCAT(first, second) + +#define QUEUE_TESTS_SLIST_CONCAT(first, second) \ + SLIST_CONCAT(first, second, slist_id_elem, ie_entry) + +/* + * ATF test registration. + */ + +#define QUEUE_TESTS_REGISTRATION(tp, type) \ + ATF_TP_ADD_TC(tp, type ## _split_after_and_concat) + +/* + * Macros defining print functions. + * + * They are currently not used in the tests above, but are useful for debugging. + */ + +#define QUEUE_TESTS_TQ_PRINT(type, hfp) \ + static void \ + type ## _print(const struct type ## _ids *const type) \ + { \ + printf(#type " %p: " __STRING(hfp ## _first) \ + " = %p, " __STRING(hfp ## _last) " = %p\n", \ + type, type->hfp ## _first, type->hfp ## _last); \ + } + +#define QUEUE_TESTS_L_PRINT(type, hfp) \ + static void \ + type ## _print(const struct type ## _ids *const type) \ + { \ + printf(#type " %p: " __STRING(hfp ## _first) " = %p\n", \ + type, type->hfp ## _first); \ + } + + +/* + * Meat. + */ + +/* Common tests. */ +QUEUE_TESTS_COMMON(tailq, TAILQ); +QUEUE_TESTS_COMMON(list, LIST); +QUEUE_TESTS_COMMON(stailq, STAILQ); +QUEUE_TESTS_COMMON(slist, SLIST); + +/* STAILQ_REVERSE(). */ +QUEUE_TESTS_CHECK_REVERSED(stailq, STAILQ); +ATF_TC(stailq_reverse); +ATF_TC_HEAD(stailq_reverse, tc) +{ + atf_tc_set_md_var(tc, "descr", "Test STAILQ_REVERSE"); +} +ATF_TC_BODY(stailq_reverse, tc) +{ + const u_int size = 100; + struct stailq_ids *const stailq = stailq_create(size, 0); + struct stailq_ids *const empty_stailq = stailq_create(0, 0); + const struct stailq_id_elem *last; + + stailq_check(stailq, size, 0); + STAILQ_REVERSE(stailq, stailq_id_elem, ie_entry); + stailq_check_reversed(stailq, size, 0); + last = STAILQ_LAST(stailq, stailq_id_elem, ie_entry); + ATF_REQUIRE_MSG(last->ie_id == 0, + "Last element of stailq %p has id %u, expected 0", + stailq, last->ie_id); + stailq_destroy(stailq); + + STAILQ_REVERSE(empty_stailq, stailq_id_elem, ie_entry); + stailq_check(empty_stailq, 0, 0); + stailq_destroy(empty_stailq); +} + +/* + * Main. + */ +ATF_TP_ADD_TCS(tp) +{ + QUEUE_TESTS_REGISTRATION(tp, tailq); + QUEUE_TESTS_REGISTRATION(tp, list); + QUEUE_TESTS_REGISTRATION(tp, stailq); + QUEUE_TESTS_REGISTRATION(tp, slist); + ATF_TP_ADD_TC(tp, stailq_reverse); + + return (atf_no_error()); +} diff --git a/tests/sys/vfs/Makefile b/tests/sys/vfs/Makefile index 9670ec52674b..e25282b2c70a 100644 --- a/tests/sys/vfs/Makefile +++ b/tests/sys/vfs/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/vfs diff --git a/tests/sys/vm/Makefile b/tests/sys/vm/Makefile index d63610ed272a..8be4b8666fdf 100644 --- a/tests/sys/vm/Makefile +++ b/tests/sys/vm/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/vm diff --git a/tests/sys/vm/mmap_test.c b/tests/sys/vm/mmap_test.c index e5f4a81a7858..6bc30f73ca95 100644 --- a/tests/sys/vm/mmap_test.c +++ b/tests/sys/vm/mmap_test.c @@ -295,14 +295,82 @@ ATF_TC_BODY(mmap__write_only, tc) munmap(p, pagesize); } -ATF_TP_ADD_TCS(tp) +ATF_TC_WITHOUT_HEAD(mmap__maxprot_basic); +ATF_TC_BODY(mmap__maxprot_basic, tc) +{ + void *p; + int error, pagesize; + + ATF_REQUIRE((pagesize = getpagesize()) > 0); + + p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ), + MAP_ANON, -1, 0); + ATF_REQUIRE(p != MAP_FAILED); + + error = mprotect(p, pagesize, PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_EXEC); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + + ATF_REQUIRE(munmap(p, pagesize) == 0); +} + +/* Make sure that PROT_MAX applies as expected to mappings of shm objects */ +ATF_TC_WITHOUT_HEAD(mmap__maxprot_shm); +ATF_TC_BODY(mmap__maxprot_shm, tc) { + void *p; + int error, fd, pagesize; + + ATF_REQUIRE((pagesize = getpagesize()) > 0); + fd = shm_open(SHM_ANON, O_RDWR, 0644); + ATF_REQUIRE(fd >= 0); + + error = ftruncate(fd, pagesize); + ATF_REQUIRE(error == 0); + + p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ), + MAP_PRIVATE, fd, 0); + ATF_REQUIRE(p != MAP_FAILED); + + error = mprotect(p, pagesize, PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_EXEC); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + + ATF_REQUIRE(munmap(p, pagesize) == 0); + + /* Again, this time with a shared mapping. */ + p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ), + MAP_SHARED, fd, 0); + ATF_REQUIRE(p != MAP_FAILED); + + error = mprotect(p, pagesize, PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_WRITE); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + error = mprotect(p, pagesize, PROT_READ | PROT_EXEC); + ATF_REQUIRE_ERRNO(EACCES, error == -1); + + ATF_REQUIRE(munmap(p, pagesize) == 0); + + ATF_REQUIRE(close(fd) == 0); +} + +ATF_TP_ADD_TCS(tp) +{ ATF_TP_ADD_TC(tp, mmap__map_at_zero); ATF_TP_ADD_TC(tp, mmap__bad_arguments); ATF_TP_ADD_TC(tp, mmap__dev_zero_private); ATF_TP_ADD_TC(tp, mmap__dev_zero_shared); ATF_TP_ADD_TC(tp, mmap__write_only); + ATF_TP_ADD_TC(tp, mmap__maxprot_basic); + ATF_TP_ADD_TC(tp, mmap__maxprot_shm); return (atf_no_error()); } diff --git a/tests/sys/vm/soxstack/Makefile b/tests/sys/vm/soxstack/Makefile index c0b8ef095bce..f9f3bd55b50a 100644 --- a/tests/sys/vm/soxstack/Makefile +++ b/tests/sys/vm/soxstack/Makefile @@ -1,4 +1,4 @@ - +PACKAGE= tests SHLIB= soxstack SHLIB_NAME= libsoxstack.so SHLIB_MAJOR= 1 diff --git a/tests/sys/vm/soxstack/soxstack.c b/tests/sys/vm/soxstack/soxstack.c index ac7c9cf03746..ecb672c1b6dc 100644 --- a/tests/sys/vm/soxstack/soxstack.c +++ b/tests/sys/vm/soxstack/soxstack.c @@ -23,7 +23,7 @@ checkstack(void) struct kinfo_vmentry *freep, *kve; struct kinfo_proc *p; struct procstat *prstat; - uint64_t stack; + uintptr_t stack; int i, cnt; prstat = procstat_open_sysctl(); @@ -33,7 +33,7 @@ checkstack(void) freep = procstat_getvmmap(prstat, p, &cnt); assert(freep != NULL); - stack = (uint64_t)&i; + stack = (uintptr_t)&i; for (i = 0; i < cnt; i++) { kve = &freep[i]; if (stack < kve->kve_start || stack > kve->kve_end) diff --git a/tests/sys/vm/stack/Makefile b/tests/sys/vm/stack/Makefile index 4279e4f48b8f..06ffcf4d4b4a 100644 --- a/tests/sys/vm/stack/Makefile +++ b/tests/sys/vm/stack/Makefile @@ -1,4 +1,3 @@ - PACKAGE= tests TESTSDIR= ${TESTSBASE}/sys/vm/stack diff --git a/tests/sys/vmm/vmm_cred_jail.sh b/tests/sys/vmm/vmm_cred_jail.sh index 4ead7fdd897d..5b02b5dc0b42 100644 --- a/tests/sys/vmm/vmm_cred_jail.sh +++ b/tests/sys/vmm/vmm_cred_jail.sh @@ -38,7 +38,7 @@ vmm_cred_jail_host_head() } vmm_cred_jail_host_body() { - if ! kldstat -qn vmm; then + if ! -c /dev/vmmctl; then atf_skip "vmm is not loaded" fi bhyvectl --vm=testvm --create @@ -59,7 +59,7 @@ vmm_cred_jail_other_head() } vmm_cred_jail_other_body() { - if ! kldstat -qn vmm; then + if ! -c /dev/vmmctl; then atf_skip "vmm is not loaded" fi vmm_mkjail myjail1 |