diff options
Diffstat (limited to 'tests/sys')
109 files changed, 9280 insertions, 405 deletions
diff --git a/tests/sys/cam/ctl/Makefile b/tests/sys/cam/ctl/Makefile index 1333397af464..05f0831fc8b0 100644 --- a/tests/sys/cam/ctl/Makefile +++ b/tests/sys/cam/ctl/Makefile @@ -1,13 +1,19 @@ 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" diff --git a/tests/sys/cam/ctl/ctl.subr b/tests/sys/cam/ctl/ctl.subr index 868b1c809571..6cc02d774bdb 100644 --- a/tests/sys/cam/ctl/ctl.subr +++ b/tests/sys/cam/ctl/ctl.subr @@ -25,15 +25,6 @@ # (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 @@ -80,7 +71,20 @@ find_device() { done } -# Create a CTL LUN +# 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=$* @@ -95,7 +99,8 @@ create_ramdisk() { 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 ramdisk -l $lun_id > /dev/null + 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/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/cddl/zfs/tests/zfsd/Makefile b/tests/sys/cddl/zfs/tests/zfsd/Makefile index e34e24b40906..588ca6e6c145 100644 --- a/tests/sys/cddl/zfs/tests/zfsd/Makefile +++ b/tests/sys/cddl/zfs/tests/zfsd/Makefile @@ -30,6 +30,8 @@ ${PACKAGE}FILES+= zfsd_hotspare_006_pos.ksh ${PACKAGE}FILES+= zfsd_hotspare_007_pos.ksh ${PACKAGE}FILES+= zfsd_hotspare_008_neg.ksh ${PACKAGE}FILES+= zfsd_import_001_pos.ksh +${PACKAGE}FILES+= zfsd_offline_001_neg.ksh +${PACKAGE}FILES+= zfsd_offline_002_neg.ksh ${PACKAGE}FILES+= zfsd_replace_001_pos.ksh ${PACKAGE}FILES+= zfsd_replace_002_pos.ksh ${PACKAGE}FILES+= zfsd_replace_003_pos.ksh 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/zfsd/zfsd_offline_001_neg.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_001_neg.ksh new file mode 100644 index 000000000000..de7996976504 --- /dev/null +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_001_neg.ksh @@ -0,0 +1,64 @@ +#!/usr/local/bin/ksh93 -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright 2025 ConnectWise. All rights reserved. +# Use is subject to license terms. + +. $STF_SUITE/tests/hotspare/hotspare.kshlib + +verify_runnable "global" + +function cleanup +{ + $ZPOOL status $TESTPOOL + if poolexists $TESTPOOL ; then + destroy_pool $TESTPOOL + fi + + partition_cleanup +} + +function verify_assertion +{ + log_must $ZPOOL offline $TESTPOOL $FAULT_DISK + + # Wait a few seconds before verifying the state + $SLEEP 10 + log_must check_state $TESTPOOL "$FAULT_DISK" "OFFLINE" +} + +log_onexit cleanup + +log_assert "ZFSD will not automatically reactivate a disk which has been administratively offlined" + +ensure_zfsd_running + +typeset FAULT_DISK=$DISK0 +typeset POOLDEVS="$DISK0 $DISK1 $DISK2" +set -A MY_KEYWORDS mirror raidz1 +for keyword in "${MY_KEYWORDS[@]}" ; do + log_must create_pool $TESTPOOL $keyword $POOLDEVS + verify_assertion + + destroy_pool "$TESTPOOL" +done diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_002_neg.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_002_neg.ksh new file mode 100644 index 000000000000..7d8dfc62d365 --- /dev/null +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_002_neg.ksh @@ -0,0 +1,66 @@ +#!/usr/local/bin/ksh93 -p +# +# CDDL HEADER START +# +# The contents of this file are subject to the terms of the +# Common Development and Distribution License (the "License"). +# You may not use this file except in compliance with the License. +# +# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE +# or http://www.opensolaris.org/os/licensing. +# See the License for the specific language governing permissions +# and limitations under the License. +# +# When distributing Covered Code, include this CDDL HEADER in each +# file and include the License file at usr/src/OPENSOLARIS.LICENSE. +# If applicable, add the following below this CDDL HEADER, with the +# fields enclosed by brackets "[]" replaced with your own identifying +# information: Portions Copyright [yyyy] [name of copyright owner] +# +# CDDL HEADER END +# + +# +# Copyright 2025 ConnectWise. All rights reserved. +# Use is subject to license terms. + +. $STF_SUITE/tests/hotspare/hotspare.kshlib + +verify_runnable "global" + +function cleanup +{ + $ZPOOL status $TESTPOOL + if poolexists $TESTPOOL ; then + destroy_pool $TESTPOOL + fi + + partition_cleanup +} + +function verify_assertion +{ + log_must $ZPOOL offline $TESTPOOL $FAULT_DISK + + # Wait a few seconds before verifying the state + $SLEEP 10 + log_must check_state $TESTPOOL "$FAULT_DISK" "OFFLINE" + log_must check_state $TESTPOOL "$SPARE_DISK" "AVAIL" +} + +log_onexit cleanup + +log_assert "ZFSD will not automatically activate a spare when a disk has been administratively offlined" + +ensure_zfsd_running + +typeset FAULT_DISK=$DISK0 +typeset SPARE_DISK=$DISK3 +typeset POOLDEVS="$DISK0 $DISK1 $DISK2" +set -A MY_KEYWORDS mirror raidz1 +for keyword in "${MY_KEYWORDS[@]}" ; do + log_must create_pool $TESTPOOL $keyword $POOLDEVS spare $SPARE_DISK + verify_assertion + + destroy_pool "$TESTPOOL" +done diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_test.sh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_test.sh index fe4ac4866ed3..b9924500a298 100755 --- a/tests/sys/cddl/zfs/tests/zfsd/zfsd_test.sh +++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_test.sh @@ -483,6 +483,64 @@ zfsd_autoreplace_003_pos_cleanup() ksh93 $(atf_get_srcdir)/cleanup.ksh || atf_fail "Cleanup failed" } +atf_test_case zfsd_offline_001_neg cleanup +zfsd_offline_001_neg_head() +{ + atf_set "descr" "ZFSD will not automatically reactivate a disk which has been administratively offlined" + atf_set "require.progs" "ksh93 zpool zfs" +} +zfsd_offline_001_neg_body() +{ + . $(atf_get_srcdir)/../../include/default.cfg + . $(atf_get_srcdir)/../hotspare/hotspare.cfg + . $(atf_get_srcdir)/zfsd.cfg + + verify_disk_count "$DISKS" 3 + verify_zfsd_running + ksh93 $(atf_get_srcdir)/setup.ksh || atf_fail "Setup failed" + ksh93 $(atf_get_srcdir)/zfsd_offline_001_neg.ksh + if [[ $? != 0 ]]; then + save_artifacts + atf_fail "Testcase failed" + fi +} +zfsd_offline_001_neg_cleanup() +{ + . $(atf_get_srcdir)/../../include/default.cfg + . $(atf_get_srcdir)/zfsd.cfg + + ksh93 $(atf_get_srcdir)/cleanup.ksh || atf_fail "Cleanup failed" +} + +atf_test_case zfsd_offline_002_neg cleanup +zfsd_offline_002_neg_head() +{ + atf_set "descr" "ZFSD will not automatically activate a spare when a disk has been administratively offlined" + atf_set "require.progs" "ksh93 zpool zfs" +} +zfsd_offline_002_neg_body() +{ + . $(atf_get_srcdir)/../../include/default.cfg + . $(atf_get_srcdir)/../hotspare/hotspare.cfg + . $(atf_get_srcdir)/zfsd.cfg + + verify_disk_count "$DISKS" 4 + verify_zfsd_running + ksh93 $(atf_get_srcdir)/setup.ksh || atf_fail "Setup failed" + ksh93 $(atf_get_srcdir)/zfsd_offline_002_neg.ksh + if [[ $? != 0 ]]; then + save_artifacts + atf_fail "Testcase failed" + fi +} +zfsd_offline_002_neg_cleanup() +{ + . $(atf_get_srcdir)/../../include/default.cfg + . $(atf_get_srcdir)/zfsd.cfg + + ksh93 $(atf_get_srcdir)/cleanup.ksh || atf_fail "Cleanup failed" +} + atf_test_case zfsd_replace_001_pos cleanup zfsd_replace_001_pos_head() { @@ -676,6 +734,8 @@ atf_init_test_cases() atf_add_test_case zfsd_autoreplace_001_neg atf_add_test_case zfsd_autoreplace_002_pos atf_add_test_case zfsd_autoreplace_003_pos + atf_add_test_case zfsd_offline_001_neg + atf_add_test_case zfsd_offline_002_neg atf_add_test_case zfsd_replace_001_pos atf_add_test_case zfsd_replace_002_pos atf_add_test_case zfsd_replace_003_pos diff --git a/tests/sys/file/closefrom_test.c b/tests/sys/file/closefrom_test.c index e30c5eb3d591..7dccf858c772 100644 --- a/tests/sys/file/closefrom_test.c +++ b/tests/sys/file/closefrom_test.c @@ -144,7 +144,7 @@ main(void) pid_t pid; int fd, flags, i, start; - printf("1..21\n"); + printf("1..22\n"); /* We'd better start up with fd's 0, 1, and 2 open. */ start = devnull(); @@ -356,5 +356,38 @@ main(void) fail_err("close_range"); ok("close_range(..., CLOSE_RANGE_CLOEXEC)"); + /* test CLOSE_RANGE_CLOFORK */ + for (i = 0; i < 8; i++) + (void)devnull(); + fd = highest_fd(); + start = fd - 8; + if (close_range(start + 1, start + 4, CLOSE_RANGE_CLOFORK) < 0) + fail_err("close_range(..., CLOSE_RANGE_CLOFORK)"); + flags = fcntl(start, F_GETFD); + if (flags < 0) + fail_err("fcntl(.., F_GETFD)"); + if ((flags & FD_CLOFORK) != 0) + fail("close_range", "CLOSE_RANGE_CLOFORK set close-on-exec " + "when it should not have on fd %d", start); + for (i = start + 1; i <= start + 4; i++) { + flags = fcntl(i, F_GETFD); + if (flags < 0) + fail_err("fcntl(.., F_GETFD)"); + if ((flags & FD_CLOFORK) == 0) + fail("close_range", "CLOSE_RANGE_CLOFORK did not set " + "close-on-exec on fd %d", i); + } + for (; i < start + 8; i++) { + flags = fcntl(i, F_GETFD); + if (flags < 0) + fail_err("fcntl(.., F_GETFD)"); + if ((flags & FD_CLOFORK) != 0) + fail("close_range", "CLOSE_RANGE_CLOFORK set close-on-exec " + "when it should not have on fd %d", i); + } + if (close_range(start, start + 8, 0) < 0) + fail_err("close_range"); + ok("close_range(..., CLOSE_RANGE_CLOFORK)"); + return (0); } diff --git a/tests/sys/file/dup_test.c b/tests/sys/file/dup_test.c index b024e72d0d1a..455115eda8c8 100644 --- a/tests/sys/file/dup_test.c +++ b/tests/sys/file/dup_test.c @@ -46,6 +46,8 @@ * Test #31: check if dup3(0) fails if oldfd == newfd. * Test #32: check if dup3(O_CLOEXEC) to a fd > current maximum number of * open files limit work. + * Tests #33-43 : Same as #18-26, 30 & 32 with O_CLOFORK instead of O_CLOEXEC, + * except F_DUP2FD_CLOEXEC. */ #include <sys/types.h> @@ -82,7 +84,7 @@ main(int __unused argc, char __unused *argv[]) orgfd = getafile(); - printf("1..32\n"); + printf("1..43\n"); /* If dup(2) ever work? */ if ((fd1 = dup(orgfd)) < 0) @@ -380,5 +382,99 @@ main(int __unused argc, char __unused *argv[]) printf("ok %d - dup3(O_CLOEXEC) didn't bypass NOFILE limit\n", test); + /* Does fcntl(F_DUPFD_CLOFORK) work? */ + if ((fd2 = fcntl(fd1, F_DUPFD_CLOFORK, 10)) < 0) + err(1, "fcntl(F_DUPFD_CLOFORK)"); + if (fd2 < 10) + printf("not ok %d - fcntl(F_DUPFD_CLOFORK) returned wrong fd %d\n", + ++test, fd2); + else + printf("ok %d - fcntl(F_DUPFD_CLOFORK) works\n", ++test); + + /* Was close-on-fork cleared? */ + ++test; + if (fcntl(fd2, F_GETFD) != FD_CLOFORK) + printf( + "not ok %d - fcntl(F_DUPFD_CLOFORK) didn't set close-on-fork\n", + test); + else + printf("ok %d - fcntl(F_DUPFD_CLOFORK) set close-on-fork\n", + test); + + /* Does dup3(O_CLOFORK) ever work? */ + if ((fd2 = dup3(fd1, fd1 + 1, O_CLOFORK)) < 0) + err(1, "dup3(O_CLOFORK)"); + printf("ok %d - dup3(O_CLOFORK) works\n", ++test); + + /* Do we get the right fd? */ + ++test; + if (fd2 != fd1 + 1) + printf( + "no ok %d - dup3(O_CLOFORK) didn't give us the right fd\n", + test); + else + printf("ok %d - dup3(O_CLOFORK) returned a correct fd\n", + test); + + /* Was close-on-fork set? */ + ++test; + if (fcntl(fd2, F_GETFD) != FD_CLOFORK) + printf( + "not ok %d - dup3(O_CLOFORK) didn't set close-on-fork\n", + test); + else + printf("ok %d - dup3(O_CLOFORK) set close-on-fork\n", + test); + + /* Does dup3(0) ever work? */ + if ((fd2 = dup3(fd1, fd1 + 1, 0)) < 0) + err(1, "dup3(0)"); + printf("ok %d - dup3(0) works\n", ++test); + + /* Do we get the right fd? */ + ++test; + if (fd2 != fd1 + 1) + printf( + "no ok %d - dup3(0) didn't give us the right fd\n", + test); + else + printf("ok %d - dup3(0) returned a correct fd\n", + test); + + /* Was close-on-fork cleared? */ + ++test; + if (fcntl(fd2, F_GETFD) != 0) + printf( + "not ok %d - dup3(0) didn't clear close-on-fork\n", + test); + else + printf("ok %d - dup3(0) cleared close-on-fork\n", + test); + + /* dup3() does not allow duplicating to the same fd */ + ++test; + if (dup3(fd1, fd1, O_CLOFORK) != -1) + printf( + "not ok %d - dup3(fd1, fd1, O_CLOFORK) succeeded\n", test); + else + printf("ok %d - dup3(fd1, fd1, O_CLOFORK) failed\n", test); + + ++test; + if (dup3(fd1, fd1, 0) != -1) + printf( + "not ok %d - dup3(fd1, fd1, 0) succeeded\n", test); + else + printf("ok %d - dup3(fd1, fd1, 0) failed\n", test); + + ++test; + if (getrlimit(RLIMIT_NOFILE, &rlp) < 0) + err(1, "getrlimit"); + if ((fd2 = dup3(fd1, rlp.rlim_cur + 1, O_CLOFORK)) >= 0) + printf("not ok %d - dup3(O_CLOFORK) bypassed NOFILE limit\n", + test); + else + printf("ok %d - dup3(O_CLOFORK) didn't bypass NOFILE limit\n", + test); + return (0); } diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile index d91199fd519e..a21512798597 100644 --- a/tests/sys/fs/fusefs/Makefile +++ b/tests/sys/fs/fusefs/Makefile @@ -4,6 +4,8 @@ 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. @@ -39,6 +41,7 @@ GTESTS+= nfs GTESTS+= notify GTESTS+= open GTESTS+= opendir +GTESTS+= pre-init GTESTS+= read GTESTS+= readdir GTESTS+= readlink @@ -55,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 @@ -64,11 +66,14 @@ 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" -TEST_METADATA+= timeout=10 +TEST_METADATA+= timeout=10 +TEST_METADATA+= required_kmods="fusefs" 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 @@ -87,9 +92,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/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 a05760207648..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" 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/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/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc index 35ae6c207229..e8081dea9604 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> @@ -421,7 +420,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, 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), @@ -473,7 +472,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, sprintf(fdstr, "%d", m_fuse_fd); build_iovec(&iov, &iovlen, "fd", fdstr, -1); if (m_maxread > 0) { - char val[10]; + char val[12]; snprintf(val, sizeof(val), "%d", m_maxread); build_iovec(&iov, &iovlen, "max_read=", &val, -1); @@ -530,7 +529,9 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, 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! */ @@ -544,10 +545,7 @@ MockFS::MockFS(int max_read, int max_readahead, bool allow_other, 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) @@ -788,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; diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh index 1de77767d0c9..ba6f7fded9d0 100644 --- a/tests/sys/fs/fusefs/mockfs.hh +++ b/tests/sys/fs/fusefs/mockfs.hh @@ -366,13 +366,17 @@ class MockFS { 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 27ffc8f5cbc1..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; @@ -160,6 +164,7 @@ TEST_F(Fhstat, lookup_dot_error) 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; @@ -189,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; @@ -215,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; @@ -226,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; @@ -243,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 @@ -260,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; @@ -282,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; @@ -304,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; }))); @@ -335,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; @@ -345,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/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc new file mode 100644 index 000000000000..2d3257500304 --- /dev/null +++ b/tests/sys/fs/fusefs/pre-init.cc @@ -0,0 +1,226 @@ +/*- + * 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 { +public: +void SetUp() { + m_no_auto_init = true; + FuseTest::SetUp(); +} +}; + +/* + * Tests for behavior that happens before the server responds to FUSE_INIT, + * parameterized on default_permissions + */ +class PreInitP: public PreInit, + public WithParamInterface<bool> +{ +void SetUp() { + m_default_permissions = GetParam(); + PreInit::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(); +} + +/* + * If some process attempts VOP_GETATTR for the mountpoint before init is + * complete, fusefs should wait, just like it does for other VOPs. + * + * To verify that fuse_vnop_getattr does indeed wait for FUSE_INIT to complete, + * invoke the test like this: + * +> sudo cpuset -c -l 0 dtrace -i 'fbt:fusefs:fuse_internal_init_callback:' -i 'fbt:fusefs:fuse_vnop_getattr:' -c "./pre-init --gtest_filter=PI/PreInitP.getattr_before_init/0" +... +dtrace: pid 4224 has exited +CPU ID FUNCTION:NAME + 0 68670 fuse_vnop_getattr:entry + 0 68893 fuse_internal_init_callback:entry + 0 68894 fuse_internal_init_callback:return + 0 68671 fuse_vnop_getattr:return + * + * Note that fuse_vnop_getattr was entered first, but exitted last. + */ +TEST_P(PreInitP, getattr_before_init) +{ + struct stat sb; + nlink_t nlink = 12345; + + 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; + nap(); /* Allow stat() to run first */ + }))); + EXPECT_CALL(*m_mock, process( + ResultOf([=](auto in) { + return (in.header.opcode == FUSE_GETATTR && + in.header.nodeid == FUSE_ROOT_ID); + }, Eq(true)), + _) + ).WillOnce(Invoke(ReturnImmediate([=](auto& in, auto& out) { + SET_OUT_HEADER_LEN(out, attr); + out.body.attr.attr.ino = in.header.nodeid; + out.body.attr.attr.mode = S_IFDIR | 0644; + out.body.attr.attr.nlink = nlink; + out.body.attr.attr_valid = UINT64_MAX; + }))); + + EXPECT_EQ(0, stat("mountpoint", &sb)); + EXPECT_EQ(nlink, sb.st_nlink); +} + +INSTANTIATE_TEST_SUITE_P(PI, PreInitP, Bool()); diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc index d059221b2e55..125b7e2d6fc7 100644 --- a/tests/sys/fs/fusefs/utils.cc +++ b/tests/sys/fs/fusefs/utils.cc @@ -124,6 +124,7 @@ bool is_unsafe_aio_enabled(void) { class FuseEnv: public Environment { virtual void SetUp() { + check_environment(); } }; @@ -132,14 +133,6 @@ void FuseTest::SetUp() { const char *maxphys_node = "kern.maxphys"; size_t size; - /* - * 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 = sizeof(m_maxbcachebuf); ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &m_maxbcachebuf, &size, NULL, 0)) << strerror(errno); @@ -158,7 +151,8 @@ void FuseTest::SetUp() { 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 9dd8dad6b5cc..91bbba909672 100644 --- a/tests/sys/fs/fusefs/utils.hh +++ b/tests/sys/fs/fusefs/utils.hh @@ -69,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; @@ -95,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/xattr.cc b/tests/sys/fs/fusefs/xattr.cc index b1cbb9ffa768..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() { @@ -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/kern/Makefile b/tests/sys/kern/Makefile index 900c9a5b3bbe..9044b1e7e4f2 100644 --- a/tests/sys/kern/Makefile +++ b/tests/sys/kern/Makefile @@ -8,6 +8,7 @@ TESTSRC= ${SRCTOP}/contrib/netbsd-tests/kernel TESTSDIR= ${TESTSBASE}/sys/kern ATF_TESTS_C+= basic_signal +ATF_TESTS_C+= copy_file_range .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH} != "powerpc" && \ ${MACHINE_ARCH} != "powerpcspe" # No support for atomic_load_64 on i386 or (32-bit) powerpc @@ -15,7 +16,13 @@ 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+= exterr_test ATF_TESTS_C+= fdgrowtable_test +ATF_TESTS_C+= getdirentries_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 @@ -75,12 +82,15 @@ PROGS+= coredump_phnum_helper PROGS+= pdeathsig_helper PROGS+= sendfile_helper +LIBADD.copy_file_range+= md +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 @@ -92,6 +102,9 @@ 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 diff --git a/tests/sys/kern/copy_file_range.c b/tests/sys/kern/copy_file_range.c new file mode 100644 index 000000000000..ca52eaf668e3 --- /dev/null +++ b/tests/sys/kern/copy_file_range.c @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/mman.h> +#include <sys/stat.h> + +#include <errno.h> +#include <fcntl.h> +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> + +#include <atf-c.h> +#include <sha256.h> + +/* + * Create a file with random data and size between 1B and 32MB. Return a file + * descriptor for the file. + */ +static int +genfile(void) +{ + char buf[256], file[NAME_MAX]; + size_t sz; + int fd; + + sz = (random() % (32 * 1024 * 1024ul)) + 1; + + snprintf(file, sizeof(file), "testfile.XXXXXX"); + fd = mkstemp(file); + ATF_REQUIRE(fd != -1); + + while (sz > 0) { + ssize_t n; + int error; + + error = getentropy(buf, sizeof(buf)); + ATF_REQUIRE(error == 0); + n = write(fd, buf, sizeof(buf) < sz ? sizeof(buf) : sz); + ATF_REQUIRE(n > 0); + + sz -= n; + } + + ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0); + return (fd); +} + +/* + * Return true if the file data in the two file descriptors is the same, + * false otherwise. + */ +static bool +cmpfile(int fd1, int fd2) +{ + struct stat st1, st2; + void *addr1, *addr2; + size_t sz; + int res; + + ATF_REQUIRE(fstat(fd1, &st1) == 0); + ATF_REQUIRE(fstat(fd2, &st2) == 0); + if (st1.st_size != st2.st_size) + return (false); + + sz = st1.st_size; + addr1 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd1, 0); + ATF_REQUIRE(addr1 != MAP_FAILED); + addr2 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd2, 0); + ATF_REQUIRE(addr2 != MAP_FAILED); + + res = memcmp(addr1, addr2, sz); + + ATF_REQUIRE(munmap(addr1, sz) == 0); + ATF_REQUIRE(munmap(addr2, sz) == 0); + + return (res == 0); +} + +/* + * Exercise a few error paths in the copy_file_range() syscall. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_invalid); +ATF_TC_BODY(copy_file_range_invalid, tc) +{ + off_t off1, off2; + int fd1, fd2; + + fd1 = genfile(); + fd2 = genfile(); + + /* Can't copy a file to itself without explicit offsets. */ + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, NULL, fd1, NULL, SSIZE_MAX, 0) == -1); + + /* When copying a file to itself, ranges cannot overlap. */ + off1 = off2 = 0; + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, &off1, fd1, &off2, 1, 0) == -1); + + /* Negative offsets are not allowed. */ + off1 = -1; + off2 = 0; + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd1, &off1, fd2, &off2, 42, 0) == -1); + ATF_REQUIRE_ERRNO(EINVAL, + copy_file_range(fd2, &off2, fd1, &off1, 42, 0) == -1); +} + +/* + * Make sure that copy_file_range() updates the file offsets passed to it. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_offset); +ATF_TC_BODY(copy_file_range_offset, tc) +{ + struct stat sb; + off_t off1, off2; + ssize_t n; + int fd1, fd2; + + off1 = off2 = 0; + + fd1 = genfile(); + fd2 = open("copy", O_RDWR | O_CREAT, 0644); + ATF_REQUIRE(fd2 != -1); + + ATF_REQUIRE(fstat(fd1, &sb) == 0); + + ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0); + ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0); + + do { + off_t ooff1, ooff2; + + ooff1 = off1; + ooff2 = off2; + n = copy_file_range(fd1, &off1, fd2, &off2, sb.st_size, 0); + ATF_REQUIRE(n >= 0); + ATF_REQUIRE_EQ(off1, ooff1 + n); + ATF_REQUIRE_EQ(off2, ooff2 + n); + } while (n != 0); + + /* Offsets should have been adjusted by copy_file_range(). */ + ATF_REQUIRE_EQ(off1, sb.st_size); + ATF_REQUIRE_EQ(off2, sb.st_size); + /* Seek offsets should have been left alone. */ + ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0); + ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0); + /* Make sure the file contents are the same. */ + ATF_REQUIRE_MSG(cmpfile(fd1, fd2), "file contents differ"); + + ATF_REQUIRE(close(fd1) == 0); + ATF_REQUIRE(close(fd2) == 0); +} + +/* + * Make sure that copying to a larger file doesn't cause it to be truncated. + */ +ATF_TC_WITHOUT_HEAD(copy_file_range_truncate); +ATF_TC_BODY(copy_file_range_truncate, tc) +{ + struct stat sb, sb1, sb2; + char digest1[65], digest2[65]; + off_t off; + ssize_t n; + int fd1, fd2; + + fd1 = genfile(); + fd2 = genfile(); + + ATF_REQUIRE(fstat(fd1, &sb1) == 0); + ATF_REQUIRE(fstat(fd2, &sb2) == 0); + + /* fd1 refers to the smaller file. */ + if (sb1.st_size > sb2.st_size) { + int tmp; + + tmp = fd1; + fd1 = fd2; + fd2 = tmp; + ATF_REQUIRE(fstat(fd1, &sb1) == 0); + ATF_REQUIRE(fstat(fd2, &sb2) == 0); + } + + /* + * Compute a hash of the bytes in the larger file which lie beyond the + * length of the smaller file. + */ + SHA256_FdChunk(fd2, digest1, sb1.st_size, sb2.st_size - sb1.st_size); + ATF_REQUIRE(lseek(fd2, 0, SEEK_SET) == 0); + + do { + n = copy_file_range(fd1, NULL, fd2, NULL, SSIZE_MAX, 0); + ATF_REQUIRE(n >= 0); + } while (n != 0); + + /* Validate file offsets after the copy. */ + off = lseek(fd1, 0, SEEK_CUR); + ATF_REQUIRE(off == sb1.st_size); + off = lseek(fd2, 0, SEEK_CUR); + ATF_REQUIRE(off == sb1.st_size); + + /* The larger file's size should remain the same. */ + ATF_REQUIRE(fstat(fd2, &sb) == 0); + ATF_REQUIRE(sb.st_size == sb2.st_size); + + /* The bytes beyond the end of the copy should be unchanged. */ + SHA256_FdChunk(fd2, digest2, sb1.st_size, sb2.st_size - sb1.st_size); + ATF_REQUIRE_MSG(strcmp(digest1, digest2) == 0, + "trailing file contents differ after copy_file_range()"); + + /* + * Verify that the copy actually replicated bytes from the smaller file. + */ + ATF_REQUIRE(ftruncate(fd2, sb1.st_size) == 0); + ATF_REQUIRE(cmpfile(fd1, fd2)); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, copy_file_range_invalid); + ATF_TP_ADD_TC(tp, copy_file_range_offset); + ATF_TP_ADD_TC(tp, copy_file_range_truncate); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/exterr_test.c b/tests/sys/kern/exterr_test.c new file mode 100644 index 000000000000..17c84c1f8ed4 --- /dev/null +++ b/tests/sys/kern/exterr_test.c @@ -0,0 +1,108 @@ +/*- + * Copyright (C) 2025 ConnectWise, LLC. 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 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/exterrvar.h> +#include <sys/mman.h> + +#include <atf-c.h> +#include <errno.h> +#include <exterr.h> +#include <stdio.h> + +ATF_TC(gettext_extended); +ATF_TC_HEAD(gettext_extended, tc) +{ + atf_tc_set_md_var(tc, "descr", "Retrieve an extended error message"); +} +ATF_TC_BODY(gettext_extended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + /* + * Use an invalid call to mmap() because it supports extended error + * messages, requires no special resources, and does not need root. + */ + ATF_CHECK_ERRNO(ENOTSUP, + mmap(NULL, 0, PROT_MAX(PROT_READ) | PROT_WRITE, 0, -1, 0)); + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + printf("Extended error: %s\n", exterr); + /* Note: error string may need to be updated due to kernel changes */ + ATF_CHECK(strstr(exterr, "prot is not subset of max_prot") != 0); +} + +ATF_TC(gettext_noextended); +ATF_TC_HEAD(gettext_noextended, tc) +{ + atf_tc_set_md_var(tc, "descr", + "Fail to retrieve an extended error message because none exists"); +} +ATF_TC_BODY(gettext_noextended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + ATF_CHECK_ERRNO(EINVAL, exterrctl(EXTERRCTL_UD, 0, NULL)); + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + ATF_CHECK_STREQ(exterr, ""); +} + +ATF_TC(gettext_noextended_after_extended); +ATF_TC_HEAD(gettext_noextended_after_extended, tc) +{ + atf_tc_set_md_var(tc, "descr", + "uexterr_gettext should not return a stale extended error message"); +} +ATF_TC_BODY(gettext_noextended_after_extended, tc) +{ + char exterr[UEXTERROR_MAXLEN]; + int r; + + /* + * First do something that will create an extended error message, but + * ignore it. + */ + ATF_CHECK_ERRNO(ENOTSUP, + mmap(NULL, 0, PROT_MAX(PROT_READ) | PROT_WRITE, 0, -1, 0)); + + /* Then do something that won't create an extended error message */ + ATF_CHECK_ERRNO(EINVAL, exterrctl(EXTERRCTL_UD, 0, NULL)); + + /* Hopefully we won't see the stale extended error message */ + r = uexterr_gettext(exterr, sizeof(exterr)); + ATF_CHECK_EQ(0, r); + ATF_CHECK_STREQ(exterr, ""); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, gettext_extended); + ATF_TP_ADD_TC(tp, gettext_noextended); + ATF_TP_ADD_TC(tp, gettext_noextended_after_extended); + + return (atf_no_error()); +} diff --git a/tests/sys/kern/getdirentries_test.c b/tests/sys/kern/getdirentries_test.c new file mode 100644 index 000000000000..e66872ffe5b6 --- /dev/null +++ b/tests/sys/kern/getdirentries_test.c @@ -0,0 +1,172 @@ +/*- + * Copyright (c) 2025 Klara, Inc. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <sys/stat.h> +#include <sys/mount.h> + +#include <dirent.h> +#include <fcntl.h> +#include <errno.h> +#include <stdint.h> + +#include <atf-c.h> + +ATF_TC(getdirentries_ok); +ATF_TC_HEAD(getdirentries_ok, tc) +{ + atf_tc_set_md_var(tc, "descr", "Successfully read a directory."); +} +ATF_TC_BODY(getdirentries_ok, tc) +{ + char dbuf[4096]; + struct dirent *d; + off_t base; + ssize_t ret; + int dd, n; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0); + ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_REQUIRE_EQ(base, lseek(dd, 0, SEEK_CUR)); + ATF_CHECK_EQ(0, close(dd)); + for (n = 0, d = (struct dirent *)dbuf; + d < (struct dirent *)(dbuf + ret); + d = (struct dirent *)((char *)d + d->d_reclen), n++) + /* nothing */ ; + ATF_CHECK_EQ((struct dirent *)(dbuf + ret), d); + ATF_CHECK_EQ(2, n); +} + +ATF_TC(getdirentries_ebadf); +ATF_TC_HEAD(getdirentries_ebadf, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "from an invalid descriptor."); +} +ATF_TC_BODY(getdirentries_ebadf, tc) +{ + char dbuf[4096]; + off_t base; + int fd; + + ATF_REQUIRE((fd = open("file", O_CREAT | O_WRONLY, 0644)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EBADF, errno); + ATF_REQUIRE_EQ(0, close(fd)); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EBADF, errno); +} + +ATF_TC(getdirentries_efault); +ATF_TC_HEAD(getdirentries_efault, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "to an invalid buffer."); +} +ATF_TC_BODY(getdirentries_efault, tc) +{ + char dbuf[4096]; + off_t base, *basep; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(dd, NULL, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EFAULT, errno); + basep = NULL; + basep++; + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), basep)); + ATF_CHECK_EQ(EFAULT, errno); + ATF_CHECK_EQ(0, close(dd)); +} + +ATF_TC(getdirentries_einval); +ATF_TC_HEAD(getdirentries_einval, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "with various invalid parameters."); +} +ATF_TC_BODY(getdirentries_einval, tc) +{ + struct statfs fsb; + char dbuf[4096]; + off_t base; + ssize_t ret; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(0, fstatfs(dd, &fsb)); + /* nbytes too small */ + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, 8, &base)); + ATF_CHECK_EQ(EINVAL, errno); + /* nbytes too big */ + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, SIZE_MAX, &base)); + ATF_CHECK_EQ(EINVAL, errno); + /* invalid position */ + ATF_REQUIRE((ret = getdirentries(dd, dbuf, sizeof(dbuf), &base)) > 0); + ATF_REQUIRE_EQ(0, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_REQUIRE(base > 0); + ATF_REQUIRE_EQ(base + 3, lseek(dd, 3, SEEK_CUR)); + /* known to fail on ufs (FFS2) and zfs, and work on tmpfs */ + if (strcmp(fsb.f_fstypename, "ufs") == 0 || + strcmp(fsb.f_fstypename, "zfs") == 0) { + atf_tc_expect_fail("incorrectly returns 0 instead of EINVAL " + "on %s", fsb.f_fstypename); + } + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(EINVAL, errno); + ATF_CHECK_EQ(0, close(dd)); +} + +ATF_TC(getdirentries_enoent); +ATF_TC_HEAD(getdirentries_enoent, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "after it is deleted."); +} +ATF_TC_BODY(getdirentries_enoent, tc) +{ + char dbuf[4096]; + off_t base; + int dd; + + ATF_REQUIRE_EQ(0, mkdir("dir", 0755)); + ATF_REQUIRE((dd = open("dir", O_DIRECTORY | O_RDONLY)) >= 0); + ATF_REQUIRE_EQ(0, rmdir("dir")); + ATF_REQUIRE_EQ(-1, getdirentries(dd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(ENOENT, errno); +} + +ATF_TC(getdirentries_enotdir); +ATF_TC_HEAD(getdirentries_enotdir, tc) +{ + atf_tc_set_md_var(tc, "descr", "Attempt to read a directory " + "from a descriptor not associated with a directory."); +} +ATF_TC_BODY(getdirentries_enotdir, tc) +{ + char dbuf[4096]; + off_t base; + int fd; + + ATF_REQUIRE((fd = open("file", O_CREAT | O_RDWR, 0644)) >= 0); + ATF_REQUIRE_EQ(-1, getdirentries(fd, dbuf, sizeof(dbuf), &base)); + ATF_CHECK_EQ(ENOTDIR, errno); + ATF_CHECK_EQ(0, close(fd)); +} + +ATF_TP_ADD_TCS(tp) +{ + ATF_TP_ADD_TC(tp, getdirentries_ok); + ATF_TP_ADD_TC(tp, getdirentries_ebadf); + ATF_TP_ADD_TC(tp, getdirentries_efault); + ATF_TP_ADD_TC(tp, getdirentries_einval); + ATF_TP_ADD_TC(tp, getdirentries_enoent); + ATF_TP_ADD_TC(tp, getdirentries_enotdir); + return (atf_no_error()); +} diff --git a/tests/sys/kern/inotify_test.c b/tests/sys/kern/inotify_test.c new file mode 100644 index 000000000000..713db55afc22 --- /dev/null +++ b/tests/sys/kern/inotify_test.c @@ -0,0 +1,864 @@ +/*- + * 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, unsigned int event, unsigned 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, unsigned int event, unsigned 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/ptrace_test.c b/tests/sys/kern/ptrace_test.c index db681293f043..fee0bd2ffa38 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); @@ -3239,7 +3239,7 @@ ATF_TC_BODY(ptrace__PT_REGSET, tc) ATF_REQUIRE(ptrace(PT_GETREGSET, wpid, (caddr_t)&vec, NT_ARM_ADDR_MASK) != -1); REQUIRE_EQ(addr_mask.code, addr_mask.data); - ATF_REQUIRE(addr_mask.code == 0 || + ATF_REQUIRE(addr_mask.code == 0xff00000000000000ul || addr_mask.code == 0xff7f000000000000UL); #endif @@ -4378,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; @@ -4461,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); @@ -4529,6 +4658,8 @@ ATF_TP_ADD_TCS(tp) 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/socket_splice.c b/tests/sys/kern/socket_splice.c index 3a85ae91ecc7..dfd4cb4f5957 100644 --- a/tests/sys/kern/socket_splice.c +++ b/tests/sys/kern/socket_splice.c @@ -84,7 +84,7 @@ tcp_socketpair(int out[2], int domain) 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_addr.s_addr = htonl(INADDR_LOOPBACK); sin.sin_port = htons(0); sinp = (struct sockaddr *)&sin; } else { @@ -92,7 +92,7 @@ tcp_socketpair(int out[2], int domain) memset(&sin6, 0, sizeof(sin6)); sin6.sin6_family = AF_INET6; sin6.sin6_len = sizeof(sin6); - sin6.sin6_addr = in6addr_any; + sin6.sin6_addr = in6addr_loopback; sin6.sin6_port = htons(0); sinp = (struct sockaddr *)&sin6; } diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile index c362793a8b64..8628ab79875f 100644 --- a/tests/sys/kern/tty/Makefile +++ b/tests/sys/kern/tty/Makefile @@ -5,8 +5,11 @@ 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/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_passfd_test.c b/tests/sys/kern/unix_passfd_test.c index 74095859d899..7dc4541ad402 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> @@ -376,6 +380,30 @@ ATF_TC_BODY(simple_send_fd_msg_cmsg_cloexec, tc) } /* + * Like simple_send_fd but also sets MSG_CMSG_CLOFORK and checks that the + * received file descriptor has the FD_CLOFORK flag set. + */ +ATF_TC_WITHOUT_HEAD(simple_send_fd_msg_cmsg_clofork); +ATF_TC_BODY(simple_send_fd_msg_cmsg_clofork, tc) +{ + struct stat getfd_stat, putfd_stat; + int fd[2], getfd, putfd; + + domainsocketpair(fd); + tempfile(&putfd); + dofstat(putfd, &putfd_stat); + sendfd(fd[0], putfd); + recvfd(fd[1], &getfd, MSG_CMSG_CLOFORK); + dofstat(getfd, &getfd_stat); + samefile(&putfd_stat, &getfd_stat); + ATF_REQUIRE_EQ_MSG(fcntl(getfd, F_GETFD) & FD_CLOFORK, FD_CLOFORK, + "FD_CLOFORK not set on the received file descriptor"); + close(putfd); + close(getfd); + closesocketpair(fd); +} + +/* * Same as simple_send_fd, only close the file reference after sending, so that * the only reference is the descriptor in the UNIX domain socket buffer. */ @@ -544,6 +572,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,16 +1060,147 @@ 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) { ATF_TP_ADD_TC(tp, simple_send_fd); ATF_TP_ADD_TC(tp, simple_send_fd_msg_cmsg_cloexec); + ATF_TP_ADD_TC(tp, simple_send_fd_msg_cmsg_clofork); ATF_TP_ADD_TC(tp, send_and_close); ATF_TP_ADD_TC(tp, send_and_cancel); 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 +1210,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 d142e228b036..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) @@ -1197,8 +1229,6 @@ ATF_TC_BODY(random_eor_and_waitall, tc) size_t off; int fd[2], eor; - atf_tc_skip("https://bugs.freebsd.org/279354"); - arc4random_buf(params.seed, sizeof(params.seed)); printf("Using seed:"); for (u_int i = 0; i < (u_int)sizeof(params.seed)/sizeof(u_short); i++) @@ -1312,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 d93bbeff4e41..bb811f78f620 100644 --- a/tests/sys/kern/unix_stream.c +++ b/tests/sys/kern/unix_stream.c @@ -28,6 +28,7 @@ #include <sys/cdefs.h> #include <sys/socket.h> #include <sys/event.h> +#include <sys/select.h> #include <sys/sysctl.h> #include <sys/un.h> #include <errno.h> @@ -35,6 +36,8 @@ #include <stdio.h> #include <stdlib.h> #include <poll.h> +#include <pthread.h> +#include <pthread_np.h> #include <atf-c.h> @@ -99,48 +102,83 @@ 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_writable(int fd, int expect) +check_select(struct check_ctx *ctx) { - fd_set wrfds; - struct pollfd pfd[1]; - struct kevent kev; - int nfds, kq; + fd_set fds; + int nfds; - FD_ZERO(&wrfds); - FD_SET(fd, &wrfds); - nfds = select(fd + 1, NULL, &wrfds, NULL, - &(struct timeval){.tv_usec = 1000}); - ATF_REQUIRE_MSG(nfds == expect, + 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 = fd, - .events = POLLOUT | POLLWRNORM, + .fd = ctx->sv[0], + .events = ctx->poll_events, }; - nfds = poll(pfd, 1, 1); - ATF_REQUIRE_MSG(nfds == expect, + 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, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL); - ATF_REQUIRE(kevent(kq, &kev, 1, NULL, 0, NULL) == 0); - nfds = kevent(kq, NULL, 0, &kev, 1, - &(struct timespec){.tv_nsec = 1000000}); - ATF_REQUIRE_MSG(nfds == expect, - "kevent() returns %d errno %d", nfds, errno); + 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); } -/* - * Make sure that a full socket is not reported as writable by event APIs. - */ -ATF_TC_WITHOUT_HEAD(full_not_writable); -ATF_TC_BODY(full_not_writable, tc) +static void +full_socketpair(int *sv) { void *buf; u_long sendspace; - int sv[2]; sendspace = getsendspace(); ATF_REQUIRE((buf = malloc(sendspace)) != NULL); @@ -149,24 +187,301 @@ ATF_TC_BODY(full_not_writable, tc) 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); +} - check_writable(sv[0], 0); +/* + * 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); - /* Read some data and re-check. */ - ATF_REQUIRE(read(sv[1], buf, sendspace / 2) == (ssize_t)sendspace / 2); + /* Now check that thread was successfully woken up and exited. */ + ATF_REQUIRE(pthread_join(thr, NULL) == 0); - check_writable(sv[0], 1); + /* Extra check repeating what joined thread already did. */ + ctx->method(ctx); + close(ctx->sv[0]); + close(ctx->sv[1]); free(buf); - close(sv[0]); - close(sv[1]); +} + +/* + * 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, full_not_writable); + 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/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/bsdextended/Makefile b/tests/sys/mac/bsdextended/Makefile index 69cd27c0e321..cc3a3f8ea534 100644 --- a/tests/sys/mac/bsdextended/Makefile +++ b/tests/sys/mac/bsdextended/Makefile @@ -9,5 +9,6 @@ TEST_METADATA.ugidfw_test+= required_user="root" # Each test case of matches_test reuses the same ruleset number, so they cannot # be run simultaneously TEST_METADATA.matches_test+= is_exclusive=true +TEST_METADATA+= required_kmods="mac_bsdextended" .include <bsd.test.mk> diff --git a/tests/sys/mac/bsdextended/matches_test.sh b/tests/sys/mac/bsdextended/matches_test.sh index 2a28be0f231b..41fa04f221e3 100644 --- a/tests/sys/mac/bsdextended/matches_test.sh +++ b/tests/sys/mac/bsdextended/matches_test.sh @@ -12,9 +12,6 @@ gidoutrange="daemon" # We expect $uidinrange in this group check_ko() { - if ! sysctl -N security.mac.bsdextended >/dev/null 2>&1; then - atf_skip "mac_bsdextended(4) support isn't available" - fi if [ $(sysctl -n security.mac.bsdextended.enabled) = "0" ]; then # The kernel module is loaded but disabled. Enable it for the # duration of the test. diff --git a/tests/sys/mac/portacl/Makefile b/tests/sys/mac/portacl/Makefile index c9fb6bbaae3e..856a85d331d5 100644 --- a/tests/sys/mac/portacl/Makefile +++ b/tests/sys/mac/portacl/Makefile @@ -10,6 +10,7 @@ TAP_TESTS_SH+= root_test .for t in ${TAP_TESTS_SH} TEST_METADATA.$t+= required_user="root" TEST_METADATA.$t+= timeout="450" +TEST_METADATA.$t+= is_exclusive="true" .endfor .include <bsd.test.mk> diff --git a/tests/sys/net/Makefile b/tests/sys/net/Makefile index 95ab86156a0a..e390c6e8059d 100644 --- a/tests/sys/net/Makefile +++ b/tests/sys/net/Makefile @@ -6,7 +6,8 @@ BINDIR= ${TESTSDIR} 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 +16,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 @@ -38,6 +40,7 @@ ${PACKAGE}FILESMODE_stp.py= 0555 MAN= PROGS+= randsleep +PROGS+= transient_tuntap CFLAGS+= -I${.CURDIR:H:H} 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 46ebb17edbdc..0c19903714b1 100755 --- a/tests/sys/net/if_bridge_test.sh +++ b/tests/sys/net/if_bridge_test.sh @@ -245,7 +245,8 @@ static_body() jexec one ifconfig ${bridge} static ${epair}a 00:01:02:03:04:05 # List addresses - atf_check -s exit:0 -o ignore \ + atf_check -s exit:0 \ + -o match:"00:01:02:03:04:05 Vlan0 ${epair}a 0 flags=1<STATIC>" \ jexec one ifconfig ${bridge} addr # Delete with bad address format @@ -266,6 +267,72 @@ static_cleanup() vnet_cleanup } +atf_test_case "vstatic" "cleanup" +vstatic_head() +{ + atf_set descr 'Bridge VLAN static address test' + atf_set require.user root +} + +vstatic_body() +{ + vnet_init + vnet_init_bridge + + epair=$(vnet_mkepair) + bridge=$(vnet_mkbridge) + + vnet_mkjail one ${bridge} ${epair}a + + ifconfig ${epair}b up + + jexec one ifconfig ${bridge} up + jexec one ifconfig ${epair}a up + jexec one ifconfig ${bridge} addm ${epair}a + + # Wrong interface + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} static ${epair}b 00:01:02:03:04:05 vlan 10 + + # Bad address format + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} static ${epair}a 00:01:02:03:04 vlan 10 + + # Invalid VLAN ID + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} static ${epair}a 00:01:02:03:04:05 vlan 5000 + + # Correct add + atf_check -s exit:0 -o ignore jexec one \ + ifconfig ${bridge} static ${epair}a 00:01:02:03:04:05 vlan 10 + + # List addresses + atf_check -s exit:0 \ + -o match:"00:01:02:03:04:05 Vlan10 ${epair}a 0 flags=1<STATIC>" \ + jexec one ifconfig ${bridge} addr + + # Delete with bad address format + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} deladdr 00:01:02:03:04 vlan 10 + + # Delete with unlisted address + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} deladdr 00:01:02:03:04:06 vlan 10 + + # Delete with wrong vlan id + atf_check -s exit:1 -o ignore -e ignore jexec one \ + ifconfig ${bridge} deladdr 00:01:02:03:04:05 vlan 20 + + # Correct delete + atf_check -s exit:0 -o ignore jexec one \ + ifconfig ${bridge} deladdr 00:01:02:03:04:05 vlan 10 +} + +vstatic_cleanup() +{ + vnet_cleanup +} + atf_test_case "span" "cleanup" span_head() { @@ -537,7 +604,7 @@ get_mtu() { intf=$1 - ifconfig ${intf} ether | awk '$5 == "mtu" { print $6 }' + ifconfig ${intf} | awk '$5 == "mtu" { print $6 }' } check_mtu() @@ -546,7 +613,7 @@ check_mtu() expected=$2 mtu=$(get_mtu $intf) - if [ $mtu -ne $expected ]; + if [ "$mtu" -ne "$expected" ]; then atf_fail "Expected MTU of $expected on $intf but found $mtu" fi @@ -703,12 +770,672 @@ 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 and vlanfilter 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} vlanfilter up + ifconfig ${epone}a up + ifconfig ${eptwo}a up + ifconfig ${bridge} addm ${epone}a untagged 20 + ifconfig ${bridge} addm ${eptwo}a untagged 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} -ifuntagged ${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 + + atf_check -s exit:0 jexec one ifconfig ${epone}b 192.0.2.1/24 up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 ifconfig ${eptwo}a up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a untagged 20 + atf_check -s exit:0 ifconfig ${bridge} addm ${eptwo}a untagged 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 + atf_check -s exit:0 jexec one ifconfig ${epone}b up + atf_check -s exit:0 jexec one ifconfig ${epone}b.20 \ + create 192.0.2.1/24 up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b.20 \ + create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 ifconfig ${eptwo}a up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a untagged 20 + atf_check -s exit:0 ifconfig ${bridge} addm ${eptwo}a untagged 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. + + atf_check -s exit:0 jexec one ifconfig ${epone}b 192.0.2.1/24 up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a untagged 20 + atf_check -s exit:0 ifconfig ${bridge} addm ${eptwo}a tagged 20 + + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 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 + + atf_check -s exit:0 jexec one ifconfig ${epone}b up + atf_check -s exit:0 jexec one ifconfig ${epone}b.20 \ + create 192.0.2.1/24 up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b up + atf_check -s exit:0 jexec two ifconfig ${eptwo}b.20 \ + create 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 ifconfig ${eptwo}a up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a + atf_check -s exit:0 ifconfig ${bridge} addm ${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} ifuntagged ${epone}a 20 + atf_check -s exit:0 ifconfig ${bridge} ifuntagged ${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} -ifuntagged ${epone}a + atf_check -s exit:0 ifconfig ${bridge} -ifuntagged ${eptwo}a + + # Add VLANs 10-30 to the access list; now access should be allowed. + atf_check -s exit:0 ifconfig ${bridge} +iftagged ${epone}a 10-30 + atf_check -s exit:0 ifconfig ${bridge} +iftagged ${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. + atf_check -s exit:0 ifconfig ${bridge} -iftagged ${epone}a 20 + atf_check -s exit:0 ifconfig ${bridge} -iftagged ${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 'iftagged' option. +# +atf_test_case "vlan_ifconfig_iftagged" "cleanup" +vlan_ifconfig_iftagged_head() +{ + atf_set descr 'test the ifconfig iftagged option' + atf_set require.user root +} + +vlan_ifconfig_iftagged_body() +{ + vnet_init + vnet_init_bridge + + ep=$(vnet_mkepair) + bridge=$(vnet_mkbridge) + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + + atf_check -s exit:0 ifconfig ${bridge} addm ${ep}a + atf_check -s exit:0 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} iftagged ${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} iftagged ${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} +iftagged ${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} -iftagged ${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} iftagged ${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} iftagged "$bad_vlan" + done +} + +vlan_ifconfig_iftagged_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 + + atf_check -s exit:0 jexec one ifconfig ${epone}b up + atf_check -s exit:0 jexec one ifconfig ${epone}b.20 \ + create 192.0.2.1/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter up + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a tagged 20 + + svi=$(vnet_mkvlan) + atf_check -s exit:0 ifconfig ${svi} vlan 20 vlandev ${bridge} + atf_check -s exit:0 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. + + atf_check -s exit:0 jexec one ifconfig ${epone}b up + atf_check -s exit:0 jexec one \ + ifconfig ${epone}b.5 create vlanproto 802.1ad up + atf_check -s exit:0 jexec one \ + ifconfig ${epone}b.5.10 create inet 192.0.2.1/24 up + + atf_check -s exit:0 jexec two ifconfig ${eptwo}b up + atf_check -s exit:0 jexec two ifconfig \ + ${eptwo}b.5 create vlanproto 802.1ad up + atf_check -s exit:0 jexec two ifconfig \ + ${eptwo}b.5.10 create inet 192.0.2.2/24 up + + bridge=$(vnet_mkbridge) + + atf_check -s exit:0 ifconfig ${bridge} vlanfilter defqinq up + atf_check -s exit:0 ifconfig ${epone}a up + atf_check -s exit:0 ifconfig ${eptwo}a up + atf_check -s exit:0 ifconfig ${bridge} addm ${epone}a + atf_check -s exit:0 ifconfig ${bridge} addm ${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. + atf_check -s exit:0 ifconfig ${bridge} +iftagged ${epone}a 5 + atf_check -s exit:0 ifconfig ${bridge} +iftagged ${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 + + # Remove the qinq flag from one of the interfaces; traffic should + # be blocked again. + atf_check -s exit:0 ifconfig ${bridge} -qinq ${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_qinq_cleanup() +{ + vnet_cleanup +} + +# Adding a bridge SVI to a bridge should not be allowed. +atf_test_case "bridge_svi_in_bridge" "cleanup" +bridge_svi_in_bridge_head() +{ + atf_set descr 'adding a bridge SVI to a bridge is not allowed (1)' + atf_set require.user root +} + +bridge_svi_in_bridge_body() +{ + vnet_init + vnet_init_bridge + + bridge=$(vnet_mkbridge) + atf_check -s exit:0 ifconfig ${bridge}.1 create + atf_check -s exit:1 -e ignore ifconfig ${bridge} addm ${bridge}.1 +} + +bridge_svi_in_bridge_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_untagged" "cleanup" +vlan_untagged_head() +{ + atf_set descr 'bridge with two ports with untagged set' + atf_set require.user root +} + +vlan_untagged_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 20 + ifconfig ${bridge} addm ${eptwo}a untagged 30 + + # With two ports on different VLANs, traffic 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 + + # Move the second port to VLAN 20; now traffic should be passed. + atf_check -s exit:0 ifconfig ${bridge} ifuntagged ${eptwo}a 20 + 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 the first's port untagged config, now traffic should + # not pass again. + atf_check -s exit:0 ifconfig ${bridge} -ifuntagged ${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_untagged_cleanup() +{ + vnet_cleanup +} + +atf_test_case "vlan_defuntagged" "cleanup" +vlan_defuntagged_head() +{ + atf_set descr 'defuntagged (defpvid) bridge option' + atf_set require.user root +} + +vlan_defuntagged_body() +{ + vnet_init + vnet_init_bridge + + bridge=$(vnet_mkbridge) + + # Invalid VLAN IDs + atf_check -s exit:1 -ematch:"invalid vlan id: 0" \ + ifconfig ${bridge} defuntagged 0 + atf_check -s exit:1 -ematch:"invalid vlan id: 4095" \ + ifconfig ${bridge} defuntagged 4095 + atf_check -s exit:1 -ematch:"invalid vlan id: 5000" \ + ifconfig ${bridge} defuntagged 5000 + + # Check the bridge option is set and cleared correctly + atf_check -s exit:0 -onot-match:"defuntagged=" \ + ifconfig ${bridge} + + atf_check -s exit:0 ifconfig ${bridge} defuntagged 10 + atf_check -s exit:0 -omatch:"defuntagged=10$" \ + ifconfig ${bridge} + + atf_check -s exit:0 ifconfig ${bridge} -defuntagged + atf_check -s exit:0 -onot-match:"defuntagged=" \ + ifconfig ${bridge} + + # Check the untagged option is correctly set on a member + atf_check -s exit:0 ifconfig ${bridge} defuntagged 10 + + epair=$(vnet_mkepair) + atf_check -s exit:0 ifconfig ${bridge} addm ${epair}a + + tag=$(ifconfig ${bridge} | sed -Ene \ + "/member: ${epair}a/ { N;s/.*untagged ([0-9]+).*/\\1/p;q; }") + if [ "$tag" != "10" ]; then + atf_fail "wrong untagged vlan: ${tag}" + fi +} + +vlan_defuntagged_cleanup() +{ + vnet_cleanup +} + atf_init_test_cases() { atf_add_test_case "bridge_transmit_ipv4_unicast" atf_add_test_case "stp" atf_add_test_case "stp_vlan" atf_add_test_case "static" + atf_add_test_case "vstatic" atf_add_test_case "span" atf_add_test_case "inherit_mac" atf_add_test_case "delete_with_members" @@ -718,4 +1445,18 @@ 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_iftagged" + atf_add_test_case "vlan_svi" + atf_add_test_case "vlan_qinq" + atf_add_test_case "vlan_untagged" + atf_add_test_case "vlan_defuntagged" + atf_add_test_case "bridge_svi_in_bridge" } diff --git a/tests/sys/net/if_gif.sh b/tests/sys/net/if_gif.sh index 16b0b1a6fca0..bff88f9e75b6 100644 --- a/tests/sys/net/if_gif.sh +++ b/tests/sys/net/if_gif.sh @@ -26,14 +26,14 @@ . $(atf_get_srcdir)/../common/vnet.subr -atf_test_case "basic" "cleanup" -basic_head() +atf_test_case "4in4" "cleanup" +4in4_head() { - atf_set descr 'Basic gif(4) test' + atf_set descr 'IPv4 in IPv4 tunnel' atf_set require.user root } -basic_body() +4in4_body() { vnet_init if ! kldstat -q -m if_gif; then @@ -65,12 +65,301 @@ basic_body() jexec two ping -c 1 198.51.100.1 } -basic_cleanup() +4in4_cleanup() +{ + vnet_cleanup +} + +atf_test_case "6in4" "cleanup" +6in4_head() +{ + atf_set descr 'IPv6 in IPv4 tunnel' + atf_set require.user root +} + +6in4_body() +{ + vnet_init + if ! kldstat -q -m if_gif; then + atf_skip "This test requires if_gif" + fi + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + jexec one ifconfig ${epair}a 192.0.2.1/24 up + gone=$(jexec one ifconfig gif create) + jexec one ifconfig $gone tunnel 192.0.2.1 192.0.2.2 + jexec one ifconfig $gone inet6 no_dad 2001:db8:1::1/64 up + + vnet_mkjail two ${epair}b + jexec two ifconfig ${epair}b 192.0.2.2/24 up + gtwo=$(jexec two ifconfig gif create) + jexec two ifconfig $gtwo tunnel 192.0.2.2 192.0.2.1 + jexec two ifconfig $gtwo inet6 no_dad 2001:db8:1::2/64 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 192.0.2.2 + + # Tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:1::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:1::1 +} + +6in4_cleanup() +{ + vnet_cleanup +} + +atf_test_case "4in6" "cleanup" +4in6_head() +{ + atf_set descr 'IPv4 in IPv6 tunnel' + atf_set require.user root +} + +4in6_body() +{ + vnet_init + if ! kldstat -q -m if_gif; then + atf_skip "This test requires if_gif" + fi + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + jexec one ifconfig ${epair}a inet6 no_dad 2001:db8::1/64 up + gone=$(jexec one ifconfig gif create) + jexec one ifconfig $gone inet6 tunnel 2001:db8::1 2001:db8::2 + jexec one ifconfig $gone inet 198.51.100.1/24 198.51.100.2 up + + vnet_mkjail two ${epair}b + jexec two ifconfig ${epair}b inet6 no_dad 2001:db8::2/64 up + gtwo=$(jexec two ifconfig gif create) + jexec two ifconfig $gtwo inet6 tunnel 2001:db8::2 2001:db8::1 + jexec two ifconfig $gtwo inet 198.51.100.2/24 198.51.100.1 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8::2 + + # Tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 198.51.100.1 +} + +4in6_cleanup() +{ + vnet_cleanup +} + +atf_test_case "6in6" "cleanup" +6in6_head() +{ + atf_set descr 'IPv6 in IPv6 tunnel' + atf_set require.user root +} + +6in6_body() +{ + vnet_init + if ! kldstat -q -m if_gif; then + atf_skip "This test requires if_gif" + fi + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + jexec one ifconfig ${epair}a inet6 no_dad 2001:db8::1/64 up + gone=$(jexec one ifconfig gif create) + jexec one ifconfig $gone inet6 tunnel 2001:db8::1 2001:db8::2 + jexec one ifconfig $gone inet6 no_dad 2001:db8:1::1/64 up + + vnet_mkjail two ${epair}b + jexec two ifconfig ${epair}b inet6 no_dad 2001:db8::2/64 up + gtwo=$(jexec two ifconfig gif create) + jexec two ifconfig $gtwo inet6 tunnel 2001:db8::2 2001:db8::1 + jexec two ifconfig $gtwo inet6 no_dad 2001:db8:1::2/64 up + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8::2 + + # Tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:1::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:1::1 +} + +6in6_cleanup() +{ + vnet_cleanup +} + +atf_test_case "etherip" "cleanup" +etherip_head() +{ + atf_set descr 'EtherIP regression' + atf_set require.user root +} + +etherip_body() +{ + vnet_init + vnet_init_bridge + + if ! kldstat -q -m if_gif; then + atf_skip "This test requires if_gif" + fi + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + gone=$(jexec one ifconfig gif create) + jexec one ifconfig ${epair}a 192.0.2.1/24 up + jexec one ifconfig $gone tunnel 192.0.2.1 192.0.2.2 + jexec one ifconfig $gone 198.51.100.1/24 198.51.100.2 up + jexec one ifconfig $gone inet6 no_dad 2001:db8:1::1/64 + + bone=$(jexec one ifconfig bridge create) + jexec one ifconfig $bone addm $gone + jexec one ifconfig $bone 192.168.169.253/24 up + jexec one ifconfig $bone inet6 no_dad 2001:db8:2::1/64 + + vnet_mkjail two ${epair}b + gtwo=$(jexec two ifconfig gif create) + jexec two ifconfig ${epair}b 192.0.2.2/24 up + jexec two ifconfig $gtwo tunnel 192.0.2.2 192.0.2.1 + jexec two ifconfig $gtwo 198.51.100.2/24 198.51.100.1 up + jexec two ifconfig $gtwo inet6 no_dad 2001:db8:1::2/64 + + btwo=$(jexec two ifconfig bridge create) + jexec two ifconfig $btwo addm $gtwo + jexec two ifconfig $btwo 192.168.169.254/24 up + jexec two ifconfig $btwo inet6 no_dad 2001:db8:2::2/64 + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 192.0.2.2 + + # EtherIP tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 192.168.169.254 + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:2::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 192.168.169.253 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:2::1 + + # EtherIP should not affect normal IPv[46] over IPv4 tunnel + # See bugzilla PR 227450 + # IPv4 in IPv4 Tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 198.51.100.1 + + # IPv6 in IPv4 tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:1::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:1::1 +} + +etherip_cleanup() +{ + vnet_cleanup +} + +atf_test_case "etherip6" "cleanup" +etherip6_head() +{ + atf_set descr 'EtherIP over IPv6 regression' + atf_set require.user root +} + +etherip6_body() +{ + vnet_init + vnet_init_bridge + + if ! kldstat -q -m if_gif; then + atf_skip "This test requires if_gif" + fi + + epair=$(vnet_mkepair) + + vnet_mkjail one ${epair}a + gone=$(jexec one ifconfig gif create) + jexec one ifconfig ${epair}a inet6 no_dad 2001:db8::1/64 up + jexec one ifconfig $gone inet6 tunnel 2001:db8::1 2001:db8::2 + jexec one ifconfig $gone 198.51.100.1/24 198.51.100.2 up + jexec one ifconfig $gone inet6 no_dad 2001:db8:1::1/64 + + bone=$(jexec one ifconfig bridge create) + jexec one ifconfig $bone addm $gone + jexec one ifconfig $bone 192.168.169.253/24 up + jexec one ifconfig $bone inet6 no_dad 2001:db8:2::1/64 + + vnet_mkjail two ${epair}b + gtwo=$(jexec two ifconfig gif create) + jexec two ifconfig ${epair}b inet6 no_dad 2001:db8::2/64 up + jexec two ifconfig $gtwo inet6 tunnel 2001:db8::2 2001:db8::1 + jexec two ifconfig $gtwo 198.51.100.2/24 198.51.100.1 up + jexec two ifconfig $gtwo inet6 no_dad 2001:db8:1::2/64 + + btwo=$(jexec two ifconfig bridge create) + jexec two ifconfig $btwo addm $gtwo + jexec two ifconfig $btwo 192.168.169.254/24 up + jexec two ifconfig $btwo inet6 no_dad 2001:db8:2::2/64 + + # Sanity check + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8::2 + + # EtherIP tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 192.168.169.254 + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:2::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 192.168.169.253 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:2::1 + + # EtherIP should not affect normal IPv[46] over IPv6 tunnel + # See bugzilla PR 227450 + # IPv4 in IPv6 Tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -c 1 198.51.100.2 + atf_check -s exit:0 -o ignore \ + jexec two ping -c 1 198.51.100.1 + + # IPv6 in IPv6 tunnel test + atf_check -s exit:0 -o ignore \ + jexec one ping -6 -c 1 2001:db8:1::2 + atf_check -s exit:0 -o ignore \ + jexec two ping -6 -c 1 2001:db8:1::1 +} + +etherip6_cleanup() { vnet_cleanup } atf_init_test_cases() { - atf_add_test_case "basic" + atf_add_test_case "4in4" + atf_add_test_case "6in4" + atf_add_test_case "4in6" + atf_add_test_case "6in6" + atf_add_test_case "etherip" + atf_add_test_case "etherip6" } diff --git a/tests/sys/net/if_lagg_test.sh b/tests/sys/net/if_lagg_test.sh index 4bdb59c8aab9..e2b998599991 100755 --- a/tests/sys/net/if_lagg_test.sh +++ b/tests/sys/net/if_lagg_test.sh @@ -190,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 diff --git a/tests/sys/net/if_ovpn/if_ovpn.sh b/tests/sys/net/if_ovpn/if_ovpn.sh index 2138e0f666ec..0281e7fc273d 100644 --- a/tests/sys/net/if_ovpn/if_ovpn.sh +++ b/tests/sys/net/if_ovpn/if_ovpn.sh @@ -499,6 +499,81 @@ atf_test_case "6in6" "cleanup" ovpn_cleanup } +atf_test_case "linklocal" "cleanup" +linklocal_head() +{ + atf_set descr 'Use IPv6 link-local addresses' + atf_set require.user root + atf_set require.progs openvpn +} + +linklocal_body() +{ + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + jexec a ifconfig ${l}a inet6 fe80::a/64 up no_dad + vnet_mkjail b ${l}b + jexec b ifconfig ${l}b inet6 fe80::b/64 up no_dad + + # Sanity check + atf_check -s exit:0 -o ignore jexec a ping6 -c 1 fe80::b%${l}a + + ovpn_start a " + dev ovpn0 + dev-type tun + proto udp6 + + cipher AES-256-GCM + auth SHA256 + + local fe80::a%${l}a + server-ipv6 2001:db8:1::/64 + + 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 fe80::a%${l}b + 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 + jexec a ifconfig + + 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 +} + +linklocal_cleanup() +{ + ovpn_cleanup +} + atf_test_case "timeout_client" "cleanup" timeout_client_head() { @@ -1149,6 +1224,261 @@ destroy_unused_cleanup() ovpn_cleanup } +atf_test_case "multihome4" "cleanup" +multihome4_head() +{ + atf_set descr 'Test multihome IPv4 with OpenVPN' + atf_set require.user root + atf_set require.progs openvpn +} + +multihome4_body() +{ + pft_init + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + atf_check jexec a ifconfig ${l}a inet 192.0.2.1/24 + atf_check jexec a ifconfig ${l}a alias 192.0.2.2/24 + vnet_mkjail b ${l}b + atf_check jexec b ifconfig ${l}b inet 192.0.2.3/24 + + # Sanity check + atf_check -s exit:0 -o ignore jexec b ping -c 1 192.0.2.1 + atf_check -s exit:0 -o ignore jexec b ping -c 1 192.0.2.2 + + ovpn_start a " + dev ovpn0 + dev-type tun + proto udp4 + + cipher AES-256-GCM + auth SHA256 + + multihome + 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.2 + 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 + " + + # Block packets from the primary address, openvpn should only use the + # configured remote address. + jexec b pfctl -e + pft_set_rules b \ + "block in quick from 192.0.2.1 to any" \ + "pass all" + + # Give the tunnel time to come up + sleep 10 + + atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1 +} + +multihome4_cleanup() +{ + ovpn_cleanup + pft_cleanup +} + +multihome6_head() +{ + atf_set descr 'Test multihome IPv6 with OpenVPN' + atf_set require.user root + atf_set require.progs openvpn +} + +multihome6_body() +{ + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + atf_check jexec a ifconfig ${l}a inet6 2001:db8::1/64 no_dad + atf_check jexec a ifconfig ${l}a inet6 alias 2001:db8::2/64 no_dad + vnet_mkjail b ${l}b + atf_check jexec b ifconfig ${l}b inet6 2001:db8::3/64 no_dad + + # Sanity check + atf_check -s exit:0 -o ignore jexec b ping6 -c 1 2001:db8::1 + atf_check -s exit:0 -o ignore jexec b ping6 -c 1 2001:db8::2 + + ovpn_start a " + dev ovpn0 + dev-type tun + proto udp6 + + cipher AES-256-GCM + auth SHA256 + + multihome + server-ipv6 2001:db8:1::/64 + + 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 2001:db8::2 + 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 + " + + # Block packets from the primary address, openvpn should only use the + # configured remote address. + jexec b pfctl -e + pft_set_rules b \ + "block in quick from 2001:db8::1 to any" \ + "pass all" + + # Give the tunnel time to come up + sleep 10 + + 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 +} + +multihome6_cleanup() +{ + ovpn_cleanup +} + +atf_test_case "float" "cleanup" +float_head() +{ + atf_set descr 'Test peer float notification' + atf_set require.user root +} + +float_body() +{ + ovpn_init + + l=$(vnet_mkepair) + + vnet_mkjail a ${l}a + jexec a ifconfig ${l}a 192.0.2.1/24 up + jexec a ifconfig lo0 127.0.0.1/8 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 + + local 192.0.2.1 + 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 2 10 + + management 192.0.2.1 1234 + " + 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 2 10 + " + + # Give the tunnel time to come up + sleep 10 + + atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1 + + # We expect the client on 192.0.2.2 + if ! echo "status" | jexec a nc -N 192.0.2.1 1234 | grep 192.0.2.2; then + atf_fail "Client not found in status list!" + fi + + # Now change the client IP + jexec b ifconfig ${l}b 192.0.2.3/24 up + + # And wait for keepalives to trigger the float notification + sleep 5 + + # So the client now has the new address in userspace + if ! echo "status" | jexec a nc -N 192.0.2.1 1234 | grep 192.0.2.3; then + atf_fail "Client not found in status list!" + fi +} + +float_cleanup() +{ + ovpn_cleanup +} + atf_init_test_cases() { atf_add_test_case "4in4" @@ -1157,6 +1487,7 @@ atf_init_test_cases() atf_add_test_case "6in4" atf_add_test_case "6in6" atf_add_test_case "4in6" + atf_add_test_case "linklocal" atf_add_test_case "timeout_client" atf_add_test_case "explicit_exit" atf_add_test_case "multi_client" @@ -1165,4 +1496,7 @@ atf_init_test_cases() atf_add_test_case "chacha" atf_add_test_case "gcm_128" atf_add_test_case "destroy_unused" + atf_add_test_case "multihome4" + atf_add_test_case "multihome6" + atf_add_test_case "float" } diff --git a/tests/sys/net/if_tun_test.sh b/tests/sys/net/if_tun_test.sh index a4ffe66e04ce..f4ce7800272e 100755 --- a/tests/sys/net/if_tun_test.sh +++ b/tests/sys/net/if_tun_test.sh @@ -56,8 +56,30 @@ basic_cleanup() vnet_cleanup } +atf_test_case "transient" "cleanup" +transient_head() +{ + atf_set descr "Test transient tunnel support" + atf_set require.user root +} +transient_body() +{ + vnet_init + vnet_mkjail one + + tun=$(jexec one ifconfig tun create) + atf_check -s exit:0 -o not-empty jexec one ifconfig ${tun} + jexec one $(atf_get_srcdir)/transient_tuntap /dev/${tun} + atf_check -s not-exit:0 -e not-empty jexec one ifconfig ${tun} +} +transient_cleanup() +{ + vnet_cleanup +} + atf_init_test_cases() { atf_add_test_case "235704" atf_add_test_case "basic" + atf_add_test_case "transient" } diff --git a/tests/sys/net/if_vlan.sh b/tests/sys/net/if_vlan.sh index 424eac705b94..8122203337e2 100755 --- a/tests/sys/net/if_vlan.sh +++ b/tests/sys/net/if_vlan.sh @@ -333,6 +333,32 @@ conflict_id_cleanup() } +# If a vlan interface is in a bridge, changing the vlandev to refer to +# a bridge should not be allowed. +atf_test_case "bridge_vlandev" "cleanup" +bridge_vlandev_head() +{ + atf_set descr 'transforming a bridge member vlan into an SVI is not allowed' + atf_set require.user root +} + +bridge_vlandev_body() +{ + vnet_init + vnet_init_bridge + + bridge=$(vnet_mkbridge) + vlan=$(vnet_mkvlan) + + atf_check -s exit:0 ifconfig ${bridge} addm ${vlan} + atf_check -s exit:1 -e ignore ifconfig ${vlan} vlan 1 vlandev ${bridge} +} + +bridge_vlandev_cleanup() +{ + vnet_cleanup +} + atf_init_test_cases() { atf_add_test_case "basic" @@ -343,4 +369,5 @@ atf_init_test_cases() atf_add_test_case "qinq_setflags" atf_add_test_case "bpf_pcp" atf_add_test_case "conflict_id" + atf_add_test_case "bridge_vlandev" } diff --git a/tests/sys/net/if_wg.sh b/tests/sys/net/if_wg.sh index e5df6afface1..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) @@ -175,6 +174,7 @@ 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() @@ -183,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) @@ -268,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() @@ -275,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 @@ -316,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) @@ -361,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() @@ -368,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) @@ -424,6 +417,208 @@ 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" @@ -432,4 +627,7 @@ atf_init_test_cases() 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/transient_tuntap.c b/tests/sys/net/transient_tuntap.c new file mode 100644 index 000000000000..b0cf43064317 --- /dev/null +++ b/tests/sys/net/transient_tuntap.c @@ -0,0 +1,54 @@ +/*- + * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org> + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +/* + * This test simply configures the tunnel as transient and exits. By the time + * we return, the tunnel should be gone because the last reference disappears. + */ + +#include <sys/types.h> +#include <sys/ioctl.h> +#include <net/if_tun.h> +#include <net/if_tap.h> + +#include <assert.h> +#include <err.h> +#include <fcntl.h> +#include <string.h> +#include <unistd.h> + +int +main(int argc, char *argv[]) +{ + unsigned long tunreq; + const char *tundev; + int one = 1, tunfd; + + assert(argc > 1); + tundev = argv[1]; + + tunfd = open(tundev, O_RDWR); + assert(tunfd >= 0); + + /* + * These are technically the same request, but we'll use the technically + * correct one just in case. + */ + if (strstr(tundev, "tun") != NULL) { + tunreq = TUNSTRANSIENT; + } else { + assert(strstr(tundev, "tap") != NULL); + tunreq = TAPSTRANSIENT; + } + + if (ioctl(tunfd, tunreq, &one) == -1) + err(1, "ioctl"); + + /* Final close should destroy the tunnel automagically. */ + close(tunfd); + + return (0); +} 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/arp.sh b/tests/sys/netinet/arp.sh index c7744d5de938..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}" \ diff --git a/tests/sys/netinet/broadcast.c b/tests/sys/netinet/broadcast.c index 32e6643a3d75..e7850d513663 100644 --- a/tests/sys/netinet/broadcast.c +++ b/tests/sys/netinet/broadcast.c @@ -90,7 +90,11 @@ firstbcast(struct in_addr *out) } /* Application sends to INADDR_BROADCAST, and this goes on the wire. */ -ATF_TC_WITHOUT_HEAD(INADDR_BROADCAST); +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 = { diff --git a/tests/sys/netinet/fibs_test.sh b/tests/sys/netinet/fibs_test.sh index 5fe8f7d87641..2d0b63f8e30a 100644 --- a/tests/sys/netinet/fibs_test.sh +++ b/tests/sys/netinet/fibs_test.sh @@ -320,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} diff --git a/tests/sys/netinet/igmp.py b/tests/sys/netinet/igmp.py index 5d3b38cac38f..feb9b8b571d5 100644 --- a/tests/sys/netinet/igmp.py +++ b/tests/sys/netinet/igmp.py @@ -62,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 = { @@ -82,7 +101,7 @@ class TestIGMP(VnetTestTemplate): @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"] @@ -107,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/so_reuseport_lb_test.c b/tests/sys/netinet/so_reuseport_lb_test.c index a1b5a3f94f61..fa9d6e425884 100644 --- a/tests/sys/netinet/so_reuseport_lb_test.c +++ b/tests/sys/netinet/so_reuseport_lb_test.c @@ -505,6 +505,11 @@ ATF_TC_BODY(connect_not_bound, tc) 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); @@ -536,6 +541,11 @@ ATF_TC_BODY(connect_bound, tc) 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); diff --git a/tests/sys/netinet/socket_afinet.c b/tests/sys/netinet/socket_afinet.c index 6fc98d982602..9c718fc5a901 100644 --- a/tests/sys/netinet/socket_afinet.c +++ b/tests/sys/netinet/socket_afinet.c @@ -550,7 +550,8 @@ bind_connected_port_test(const atf_tc_t *tc, int domain) 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); 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_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 753571fbf7a1..26f1a18a8d32 100644 --- a/tests/sys/netinet6/Makefile +++ b/tests/sys/netinet6/Makefile @@ -14,7 +14,8 @@ ATF_TESTS_SH= exthdr \ lpm6 \ fibs6 \ ndp \ - proxy_ndp + proxy_ndp \ + addr6 TEST_METADATA.divert+= execenv="jail" \ execenv_jail_params="vnet allow.raw_sockets" @@ -33,6 +34,8 @@ 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 \ mld.py \ diff --git a/tests/sys/netinet6/addr6.sh b/tests/sys/netinet6/addr6.sh new file mode 100755 index 000000000000..6fd66d5aa0c7 --- /dev/null +++ b/tests/sys/netinet6/addr6.sh @@ -0,0 +1,70 @@ +#!/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_test_case "anycast_raw_addr" "cleanup" +anycast_raw_addr_head() +{ + atf_set descr "a raw socket can bind to an anycast address" + atf_set require.user root +} + +anycast_raw_addr_body() +{ + # lo0 needs to be up in the test jail for this test to work + ifconfig lo0 up + + netif=$(ifconfig lo create) + echo $netif >netif + atf_check -s exit:0 ifconfig $netif inet6 2001:db8::1/128 up + atf_check -s exit:0 ifconfig $netif inet6 2001:db8::2/128 anycast + atf_check -s exit:0 -o ignore ping -c1 -S 2001:db8::2 2001:db8::1 +} + +anycast_raw_addr_cleanup() +{ + ifconfig $(cat netif) destroy +} + +atf_init_test_cases() +{ + atf_add_test_case "addr6_invalid_addr" + atf_add_test_case "anycast_raw_addr" +} diff --git a/tests/sys/netinet6/redirect.sh b/tests/sys/netinet6/redirect.sh index aa0731d89101..40874f8c9b6d 100644 --- a/tests/sys/netinet6/redirect.sh +++ b/tests/sys/netinet6/redirect.sh @@ -39,10 +39,6 @@ valid_redirect_head() { 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/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 839127fe5232..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) { diff --git a/tests/sys/netpfil/common/dummynet.sh b/tests/sys/netpfil/common/dummynet.sh index b77b2df84010..66736fbecdb7 100644 --- a/tests/sys/netpfil/common/dummynet.sh +++ b/tests/sys/netpfil/common/dummynet.sh @@ -265,10 +265,6 @@ queue_body() { fw=$1 - if [ $fw = "ipfw" ] && [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/264805" - fi - firewall_init $fw dummynet_init $fw diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile index e3110d0e5df7..616ffe560b3a 100644 --- a/tests/sys/netpfil/pf/Makefile +++ b/tests/sys/netpfil/pf/Makefile @@ -21,8 +21,9 @@ ATF_TESTS_SH+= altq \ loginterface \ killstate \ macro \ - map_e \ match \ + max_pkt_rate \ + max_pkt_size \ max_states \ mbuf \ modulate \ @@ -54,12 +55,17 @@ ATF_TESTS_SH+= altq \ tcp \ tos +ATF_TESTS_PYTEST+= frag4.py ATF_TESTS_PYTEST+= frag6.py +ATF_TESTS_PYTEST+= header.py ATF_TESTS_PYTEST+= icmp.py +ATF_TESTS_PYTEST+= igmp.py +ATF_TESTS_PYTEST+= mld.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 # Allow tests to run in parallel in their own jails TEST_METADATA+= execenv="jail" @@ -81,7 +87,8 @@ ${PACKAGE}FILES+= \ pft_ether.py \ pft_read_ipfix.py \ rdr-srcport.py \ - utils.subr + utils.subr \ + utils.py ${PACKAGE}FILESMODE_bsnmpd.conf= 0555 ${PACKAGE}FILESMODE_CVE-2019-5597.py= 0555 diff --git a/tests/sys/netpfil/pf/anchor.sh b/tests/sys/netpfil/pf/anchor.sh index 463cd4d475e3..64ca84b34c3d 100644 --- a/tests/sys/netpfil/pf/anchor.sh +++ b/tests/sys/netpfil/pf/anchor.sh @@ -350,9 +350,9 @@ nat_body() 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" \ + atf_check -s exit:0 -o match:"nat log on ${epair}a 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" \ + atf_check -s exit:0 -o match:"rdr on ${epair}a inet proto tcp from any to any port = echo -> 127.0.0.1 port 7" \ jexec alcatraz pfctl -sn -a "*" } @@ -361,6 +361,138 @@ 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_test_case "recursive_flush" "cleanup" +recursive_flush_head() +{ + atf_set descr 'Test recursive flushing of rules' + atf_set require.user root +} + +recursive_flush_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 \ + "block" \ + "anchor \"foo\" {\n\ + pass\n\ + }" + + # We can ping thanks to the pass rule in foo + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + # Only reset the main rules. I.e. not a recursive flush + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\"" + + # "foo" still has the pass rule, so this works + jexec alcatraz pfctl -a "*" -sr + atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1 + + # Now do a recursive flush + atf_check -s exit:0 -e ignore -o ignore \ + jexec alcatraz pfctl -a "*" -Fr + pft_set_rules alcatraz \ + "block" \ + "anchor \"foo\"" + + # So this fails + jexec alcatraz pfctl -a "*" -sr + atf_check -s exit:2 -o ignore ping -c 1 192.0.2.1 +} + +recursive_flush_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "pr183198" @@ -372,4 +504,7 @@ atf_init_test_cases() 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" + atf_add_test_case "recursive_flush" } diff --git a/tests/sys/netpfil/pf/debug.sh b/tests/sys/netpfil/pf/debug.sh index 18a7febfbb5b..404d37ab8932 100644 --- a/tests/sys/netpfil/pf/debug.sh +++ b/tests/sys/netpfil/pf/debug.sh @@ -50,7 +50,57 @@ basic_cleanup() pft_cleanup } +atf_test_case "reset" "cleanup" +reset_head() +{ + atf_set descr 'Test resetting debug level' + atf_set require.user root +} + +reset_body() +{ + pft_init + + vnet_mkjail debug + + # Default is Urgent + atf_check -s exit:0 -o match:'Debug: Urgent' \ + jexec debug pfctl -sa + state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + + # Change defaults + pft_set_rules debug \ + "set limit states 42" + atf_check -s exit:0 -e ignore \ + jexec debug pfctl -x loud + + atf_check -s exit:0 -o match:'Debug: Loud' \ + jexec debug pfctl -sa + new_state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + if [ $state_limit -eq $new_state_limit ]; then + jexec debug pfctl -sa + atf_fail "Failed to change state limit" + fi + + # Reset + atf_check -s exit:0 -o ignore -e ignore \ + jexec debug pfctl -FR + atf_check -s exit:0 -o match:'Debug: Urgent' \ + jexec debug pfctl -sa + new_state_limit=$(jexec debug pfctl -sa | grep 'states.*hard limit' | awk '{ print $4; }') + if [ $state_limit -ne $new_state_limit ]; then + jexec debug pfctl -sa + atf_fail "Failed to reset state limit" + fi +} + +reset_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "basic" + atf_add_test_case "reset" } diff --git a/tests/sys/netpfil/pf/forward.sh b/tests/sys/netpfil/pf/forward.sh index 5d7d48a5dd9a..e9539bc9d278 100644 --- a/tests/sys/netpfil/pf/forward.sh +++ b/tests/sys/netpfil/pf/forward.sh @@ -101,10 +101,6 @@ v6_body() { pft_init - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/260460" - fi - epair_send=$(vnet_mkepair) epair_recv=$(vnet_mkepair) diff --git a/tests/sys/netpfil/pf/frag4.py b/tests/sys/netpfil/pf/frag4.py new file mode 100644 index 000000000000..3303d2ee7780 --- /dev/null +++ b/tests/sys/netpfil/pf/frag4.py @@ -0,0 +1,72 @@ +# +# 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 +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestFrag4_NoReassemble(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "vnet3": {"ifaces": ["if2"]}, + "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]}, + "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}, + } + + def vnet2_handler(self, vnet): + outifname = vnet.iface_alias_map["if2"].name + + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "set reassemble no", + "nat on %s from 192.0.2.0/24 to any -> (%s)" % (outifname, outifname), + "pass out" + ]) + + ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1") + + def vnet3_handler(self, vnet): + # We deliberately don't set the default gateway here, so if we get a + # reply from this we know we did NAT in vnet2 + pass + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_udp_frag(self): + ToolsHelper.print_output("/sbin/route add default 192.0.2.2") + ToolsHelper.print_output("/sbin/ping -c 3 198.51.100.2") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + + pkt = sp.IP(dst="198.51.100.2", frag=123) \ + / sp.UDP(dport=12345, sport=54321) + reply = sp.sr1(pkt, timeout=3) + # We don't expect a reply + assert not reply diff --git a/tests/sys/netpfil/pf/frag6.py b/tests/sys/netpfil/pf/frag6.py index 108b53874d0b..26ae7af7c90c 100644 --- a/tests/sys/netpfil/pf/frag6.py +++ b/tests/sys/netpfil/pf/frag6.py @@ -1,24 +1,11 @@ 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", "dummymbuf"] TOPOLOGY = { @@ -97,6 +84,96 @@ class TestFrag6(VnetTestTemplate): sp.send(pkts, inter = 0.1) +class TestFrag6HopHyHop(VnetTestTemplate): + REQUIRED_MODULES = ["pf"] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1", "if2"]}, + "vnet2": {"ifaces": ["if1", "if2"]}, + "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]}, + "if2": {"prefixes6": [("2001:db8:666::1/64", "2001:db8:1::2/64")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1") + ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::1 00:01:02:03:04:05") + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.pf_rules([ + "scrub fragment reassemble min-ttl 10", + "pass 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 = { @@ -141,3 +218,59 @@ class TestFrag6_Overlap(VnetTestTemplate): 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/header.py b/tests/sys/netpfil/pf/header.py new file mode 100644 index 000000000000..a5e36bc85d14 --- /dev/null +++ b/tests/sys/netpfil/pf/header.py @@ -0,0 +1,216 @@ +# +# 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"] + recvif = self.vnet.iface_alias_map["if2"].name + gw_mac = sendif.epairb.ether + + 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.name) + 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.name) + 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.name) + 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"] + recvif = self.vnet.iface_alias_map["if2"].name + our_mac = sendif.ether + gw_mac = sendif.epairb.ether + + 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.name) + 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.name) + 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.name) + 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 index 232c56a23dbf..c5e945d60e99 100644 --- a/tests/sys/netpfil/pf/icmp.py +++ b/tests/sys/netpfil/pf/icmp.py @@ -91,10 +91,10 @@ class TestICMP(VnetTestTemplate): def test_inner_match(self): vnet = self.vnet_map["vnet1"] dst_vnet = self.vnet_map["vnet3"] - sendif = vnet.iface_alias_map["if1"].name + sendif = vnet.iface_alias_map["if1"] - our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif) - dst_mac = re.sub("0a$", "0b", our_mac) + our_mac = sendif.ether + dst_mac = sendif.epairb.ether # Import in the correct vnet, so at to not confuse Scapy import scapy.all as sp @@ -111,7 +111,7 @@ class TestICMP(VnetTestTemplate): / sp.IP(src="192.0.2.2", dst="198.51.100.2") \ / sp.ICMP(type='echo-request') \ / "PAYLOAD" - sp.sendp(pkt, sendif, verbose=False) + sp.sendp(pkt, sendif.name, verbose=False) # Now try to pass an ICMP error message piggy-backing on that state, but # use a different source address @@ -120,7 +120,7 @@ class TestICMP(VnetTestTemplate): / 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) + sp.sendp(pkt, sendif.name, verbose=False) try: rcvd = self.wait_object(dst_vnet.pipe, timeout=1) @@ -136,8 +136,7 @@ class TestICMP(VnetTestTemplate): / 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 = sp.sr1(packet, timeout=3) p.show() ip = p.getlayer(sp.IP) @@ -175,3 +174,96 @@ class TestICMP(VnetTestTemplate): self.check_icmp_echo(sp, 128) self.check_icmp_echo(sp, 1464) self.check_icmp_echo(sp, 1468) + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_truncated_opts(self): + 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 + + packet = sp.IP(dst="198.51.100.2", flags="DF") \ + / sp.ICMP(type='dest-unreach', length=108) \ + / sp.IP(src="198.51.100.2", dst="192.0.2.2", len=1000, \ + ihl=(120 >> 2), options=[ \ + sp.IPOption_Security(length=100)]) + packet.show() + sp.sr1(packet, timeout=3) + +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/igmp.py b/tests/sys/netpfil/pf/igmp.py new file mode 100644 index 000000000000..b339a2825082 --- /dev/null +++ b/tests/sys/netpfil/pf/igmp.py @@ -0,0 +1,95 @@ +# +# 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 +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestIGMP(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass", + ]) + ToolsHelper.print_output("/sbin/pfctl -x loud") + ToolsHelper.print_output("echo \"j 230.0.0.1 %s\ns 3600\nq\" | /usr/sbin/mtest" % ifname) + + def find_igmp_reply(self, pkt, ifname): + pkt.show() + s = DelayedSend(pkt) + + found = False + packets = self.sp.sniff(iface=ifname, timeout=5) + for r in packets: + r.show() + igmp = r.getlayer(self.sc.igmp.IGMP) + if not igmp: + continue + igmp.show() + if not igmp.gaddr == "230.0.0.1": + continue + found = True + return found + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_ip_opts(self): + """Verify that we allow IGMP packets with IP options""" + ifname = self.vnet.iface_alias_map["if1"].name + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + import scapy.contrib as sc + import scapy.contrib.igmp + self.sp = sp + self.sc = sc + + # We allow IGMP packets with the router alert option + pkt = sp.IP(dst="224.0.0.1%%%s" % ifname, ttl=1, + options=[sp.IPOption_Router_Alert()]) \ + / sc.igmp.IGMP(type=0x11, mrcode=1) + assert self.find_igmp_reply(pkt, ifname) + + # But not with other options + pkt = sp.IP(dst="224.0.0.1%%%s" % ifname, ttl=1, + options=[sp.IPOption_NOP()]) \ + / sc.igmp.IGMP(type=0x11, mrcode=1) + assert not self.find_igmp_reply(pkt, ifname) + + # Or with the wrong TTL + pkt = sp.IP(dst="224.0.0.1%%%s" % ifname, ttl=2, + options=[sp.IPOption_Router_Alert()]) \ + / sc.igmp.IGMP(type=0x11, mrcode=1) + assert not self.find_igmp_reply(pkt, ifname) diff --git a/tests/sys/netpfil/pf/ioctl/validation.c b/tests/sys/netpfil/pf/ioctl/validation.c index 1ce8999dcb91..18fafe11c6ab 100644 --- a/tests/sys/netpfil/pf/ioctl/validation.c +++ b/tests/sys/netpfil/pf/ioctl/validation.c @@ -32,6 +32,7 @@ #include <net/if.h> #include <net/pfvar.h> +#include <errno.h> #include <fcntl.h> #include <stdio.h> @@ -893,6 +894,39 @@ ATF_TC_CLEANUP(rpool_mtx2, tc) COMMON_CLEANUP(); } +ATF_TC_WITH_CLEANUP(natlook); +ATF_TC_HEAD(natlook, tc) +{ + atf_tc_set_md_var(tc, "require.user", "root"); +} + +ATF_TC_BODY(natlook, tc) +{ + struct pfioc_natlook nl = { 0 }; + + COMMON_HEAD(); + + nl.af = AF_INET; + nl.proto = IPPROTO_ICMP; + nl.saddr.v4.s_addr = 0x01020304; + nl.daddr.v4.s_addr = 0x05060708; + + /* Invalid direction */ + nl.direction = 42; + + ATF_CHECK_ERRNO(EINVAL, ioctl(dev, DIOCNATLOOK, &nl) == -1); + + /* Invalid af */ + nl.direction = PF_IN; + nl.af = 99; + + ATF_CHECK_ERRNO(EAFNOSUPPORT, ioctl(dev, DIOCNATLOOK, &nl) == -1); +} + +ATF_TC_CLEANUP(natlook, tc) +{ + COMMON_CLEANUP(); +} ATF_TP_ADD_TCS(tp) { @@ -918,6 +952,7 @@ ATF_TP_ADD_TCS(tp) ATF_TP_ADD_TC(tp, tag); ATF_TP_ADD_TC(tp, rpool_mtx); ATF_TP_ADD_TC(tp, rpool_mtx2); + ATF_TP_ADD_TC(tp, natlook); return (atf_no_error()); } diff --git a/tests/sys/netpfil/pf/killstate.sh b/tests/sys/netpfil/pf/killstate.sh index 5d8e040d3cbb..0d98db822535 100644 --- a/tests/sys/netpfil/pf/killstate.sh +++ b/tests/sys/netpfil/pf/killstate.sh @@ -117,10 +117,6 @@ v6_body() { pft_init - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/260458" - fi - epair=$(vnet_mkepair) ifconfig ${epair}a inet6 2001:db8::1/64 up no_dad @@ -574,6 +570,62 @@ 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() { @@ -653,5 +705,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 index 474684bef660..69f0b6af2ccf 100644 --- a/tests/sys/netpfil/pf/limits.sh +++ b/tests/sys/netpfil/pf/limits.sh @@ -60,7 +60,60 @@ 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 59f9e7f7e14c..000000000000 --- a/tests/sys/netpfil/pf/map_e.sh +++ /dev/null @@ -1,90 +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 ${PWD}/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() -{ - pft_cleanup -} - -atf_init_test_cases() -{ - atf_add_test_case "map_e" -} 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/mbuf.sh b/tests/sys/netpfil/pf/mbuf.sh index d845f793a969..3abae65203cd 100644 --- a/tests/sys/netpfil/pf/mbuf.sh +++ b/tests/sys/netpfil/pf/mbuf.sh @@ -69,22 +69,22 @@ inet_in_mbuf_len_body() # 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_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)" + 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)" + 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)" + atf_check_equal '1' '$(jexec alcatraz sysctl -n net.dummymbuf.hits)' } inet_in_mbuf_len_cleanup() { @@ -105,6 +105,12 @@ inet6_in_mbuf_len_body() epair=$(vnet_mkepair) ifconfig ${epair}a inet6 2001:db8::1/64 up no_dad + # Ensure we don't unintentionally send MLD packets to alcatraz + pfctl -e + echo "block + pass out inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv, echoreq, echorep } + " | pfctl -g -f - + # 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 @@ -134,22 +140,22 @@ inet6_in_mbuf_len_body() # 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_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)" + 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)" + 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)" + atf_check_equal '1' '$(jexec alcatraz sysctl -n net.dummymbuf.hits)' } inet6_in_mbuf_len_cleanup() { @@ -199,29 +205,29 @@ ethernet_in_mbuf_len_body() # 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_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)" + 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)" + 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)" + 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)" + atf_check_equal '1' '$(jexec alcatraz sysctl -n net.dummymbuf.hits)' } ethernet_in_mbuf_len_cleanup() { diff --git a/tests/sys/netpfil/pf/mld.py b/tests/sys/netpfil/pf/mld.py new file mode 100644 index 000000000000..d118a34c8a7d --- /dev/null +++ b/tests/sys/netpfil/pf/mld.py @@ -0,0 +1,95 @@ +# +# 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 +from utils import DelayedSend +from atf_python.sys.net.tools import ToolsHelper +from atf_python.sys.net.vnet import VnetTestTemplate + +class TestMLD(VnetTestTemplate): + REQUIRED_MODULES = [ "pf" ] + TOPOLOGY = { + "vnet1": {"ifaces": ["if1"]}, + "vnet2": {"ifaces": ["if1"]}, + "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]}, + } + + def vnet2_handler(self, vnet): + ifname = vnet.iface_alias_map["if1"].name + #ToolsHelper.print_output("/sbin/pfctl -e") + ToolsHelper.pf_rules([ + "pass", + ]) + ToolsHelper.print_output("/sbin/pfctl -x loud") + #ToolsHelper.print_output("echo \"j 230.0.0.1 %s\ns 3600\nq\" | /usr/sbin/mtest" % ifname) + + def find_mld_reply(self, pkt, ifname): + pkt.show() + s = DelayedSend(pkt) + + found = False + packets = self.sp.sniff(iface=ifname, timeout=5) + for r in packets: + r.show() + mld = r.getlayer(self.sp.ICMPv6MLReport2) + if not mld: + continue + mld.show() + found = True + return found + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_router_alert(self): + """Verify that we allow MLD packets with router alert extension header""" + ifname = self.vnet.iface_alias_map["if1"].name + #ToolsHelper.print_output("/sbin/ifconfig %s inet6 -ifdisable" % ifname) + ToolsHelper.print_output("/sbin/ifconfig") + + # Import in the correct vnet, so at to not confuse Scapy + import scapy.all as sp + import scapy.contrib as sc + import scapy.contrib.igmp + self.sp = sp + self.sc = sc + + # A correct MLD query gets a reply + pkt = sp.IPv6(src="fe80::1%%%s" % ifname, dst="ff02::1", hlim=1) \ + / sp.RouterAlert(value=0) \ + / sp.ICMPv6MLQuery2() + assert self.find_mld_reply(pkt, ifname) + + # The wrong extension header does not + pkt = sp.IPv6(src="fe80::1%%%s" % ifname, dst="ff02::1", hlim=1) \ + / sp.IPv6ExtHdrRouting() \ + / sp.ICMPv6MLQuery2() + assert not self.find_mld_reply(pkt, ifname) + + # Neither does an incorrect hop limit + pkt = sp.IPv6(src="fe80::1%%%s" % ifname, dst="ff02::1", hlim=2) \ + / sp.RouterAlert(value=0) \ + / sp.ICMPv6MLQuery2() + assert not self.find_mld_reply(pkt, ifname) diff --git a/tests/sys/netpfil/pf/nat.sh b/tests/sys/netpfil/pf/nat.sh index f7026feb5078..5ea1dd6d8b2f 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 @@ -112,14 +114,7 @@ nested_anchor_body() } -atf_test_case "endpoint_independent" "cleanup" -endpoint_independent_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_body() +endpoint_independent_setup() { pft_init filter="udp and dst port 1234" # only capture udp pings @@ -153,13 +148,15 @@ endpoint_independent_body() 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 \ - "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a)" + pft_set_rules nat "${1}" jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ --immediate-mode $filter & @@ -198,8 +195,7 @@ endpoint_independent_body() fi # validate endpoint independent nat rule behaviour - pft_set_rules nat \ - "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a) endpoint-independent" + pft_set_rules nat "${2}" jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \ --immediate-mode $filter & @@ -238,7 +234,47 @@ endpoint_independent_body() fi } -endpoint_independent_cleanup() +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 @@ -438,14 +474,358 @@ 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_test_case "empty_pool" "cleanup" +empty_pool_head() +{ + atf_set descr 'NAT with empty pool' + atf_set require.user root + atf_set require.progs python3 scapy +} + +empty_pool_body() +{ + pft_init + setup_router_server_ipv6 + + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b" \ + "pass out on ${epair_server}a inet6 from any to ${net_server_host_server} nat-to <nonexistent>" \ + + # pf_map_addr_sn() won't be able to pick a target address, because + # the table used in redireciton pool is empty. Packet will not be + # forwarded, error counter will be increased. + ping_server_check_reply exit:1 + # Ignore warnings about not-loaded ALTQ + atf_check -o "match:map-failed +1 +" -x "jexec router pfctl -qvvsi 2> /dev/null" +} + +empty_pool_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "exhaust" atf_add_test_case "nested_anchor" - atf_add_test_case "endpoint_independent" + 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" + atf_add_test_case "empty_pool" } diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py index e64b7bbd573b..705de72f5bc4 100644 --- a/tests/sys/netpfil/pf/nat64.py +++ b/tests/sys/netpfil/pf/nat64.py @@ -28,25 +28,12 @@ import pytest import selectors import socket import sys -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 TestNAT64(VnetTestTemplate): - REQUIRED_MODULES = [ "pf" ] + REQUIRED_MODULES = [ "pf", "pflog" ] TOPOLOGY = { "vnet1": {"ifaces": ["if1"]}, "vnet2": {"ifaces": ["if1", "if2"]}, @@ -105,11 +92,16 @@ class TestNAT64(VnetTestTemplate): 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("/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]) + "block", + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }", + "pass in on %s inet6 af-to inet from 192.0.2.1" % ifname, + ]) + + vnet.pipe.send(socket.if_nametoindex("pflog0")) @pytest.mark.require_user("root") @pytest.mark.require_progs(["scapy"]) @@ -191,7 +183,7 @@ class TestNAT64(VnetTestTemplate): # Check the hop limit ip6 = reply.getlayer(sp.IPv6) - assert ip6.hlim == 62 + assert ip6.hlim == 61 @pytest.mark.require_user("root") @pytest.mark.require_progs(["scapy"]) @@ -249,7 +241,7 @@ class TestNAT64(VnetTestTemplate): 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=1) \ + 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) @@ -259,7 +251,7 @@ class TestNAT64(VnetTestTemplate): 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=1) \ + 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) @@ -269,7 +261,7 @@ class TestNAT64(VnetTestTemplate): 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=1) \ + 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) @@ -280,8 +272,87 @@ class TestNAT64(VnetTestTemplate): 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=1) \ + 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 + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_bad_len(self): + """ + PR 288224: we can panic if the IPv6 plen is longer than the packet length. + """ + 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, plen=512) \ + / sp.ICMPv6EchoRequest() / sp.Raw("foo") + reply = sp.sr1(packet, timeout=3) + # We don't expect a reply to a corrupted packet + assert not reply + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_noip6(self): + """ + PR 288263: link-local target address in icmp6 ADVERT can cause NULL deref + """ + ifname = self.vnet.iface_alias_map["if1"].name + gw_mac = self.vnet.iface_alias_map["if1"].epairb.ether + scopeid = self.wait_object(self.vnet_map["vnet2"].pipe) + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + pkt = sp.Ether(dst=gw_mac) \ + / sp.IPv6(dst="64:ff9b::203.0.113.2") \ + / sp.ICMPv6ND_NA(tgt="FFA2:%x:2821:125F:1D27:B3B2:3F6F:C43C" % scopeid) + pkt.show() + sp.hexdump(pkt) + s = DelayedSend(pkt, sendif=ifname) + + packets = sp.sniff(iface=ifname, timeout=5) + for r in packets: + r.show() + + # Try scope id that likely doesn't have an interface at all + pkt = sp.Ether(dst=gw_mac) \ + / sp.IPv6(dst="64:ff9b::203.0.113.2") \ + / sp.ICMPv6ND_NA(tgt="FFA2:%x:2821:125F:1D27:B3B2:3F6F:C43C" % 255) + pkt.show() + sp.hexdump(pkt) + s = DelayedSend(pkt, sendif=ifname) + + packets = sp.sniff(iface=ifname, timeout=5) + for r in packets: + r.show() + + @pytest.mark.require_user("root") + @pytest.mark.require_progs(["scapy"]) + def test_ttl_zero(self): + """ + PR 288274: we can use an mbuf after free on TTL = 0 + """ + ifname = self.vnet.iface_alias_map["if1"].name + gw_mac = self.vnet.iface_alias_map["if1"].epairb.ether + ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1") + + import scapy.all as sp + + pkt = sp.Ether(dst=gw_mac) \ + / sp.IPv6(dst="64:ff9b::192.0.2.2", hlim=0) \ + / sp.SCTP(sport=1111, dport=2222) \ + / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, \ + a_rwnd=1500, params=[ \ + sp.SCTPChunkParamIPv4Addr() \ + ]) + pkt.show() + sp.hexdump(pkt) + s = DelayedSend(pkt, sendif=ifname) + + packets = sp.sniff(iface=ifname, timeout=5) + for r in packets: + r.show() + diff --git a/tests/sys/netpfil/pf/nat64.sh b/tests/sys/netpfil/pf/nat64.sh index 0bba1470c4c5..4438ad6abb85 100644 --- a/tests/sys/netpfil/pf/nat64.sh +++ b/tests/sys/netpfil/pf/nat64.sh @@ -55,15 +55,19 @@ nat64_setup_base() nat64_setup_in() { + state_policy="${1:-if-bound}" nat64_setup_base pft_set_rules rtr \ "set reassemble yes" \ - "set state-policy if-bound" \ + "set state-policy ${state_policy}" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" } nat64_setup_out() { + state_policy="${1:-if-bound}" nat64_setup_base jexec rtr sysctl net.inet6.ip6.forwarding=1 # AF translation happens post-routing, traffic must be directed @@ -72,11 +76,11 @@ nat64_setup_out() 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" + "set state-policy ${state_policy}" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair}b from any to 64:ff9b::/96" \ + "pass out on ${epair_link}a from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" } atf_test_case "icmp_echo_in" "cleanup" @@ -185,14 +189,14 @@ fragmentation_out_cleanup() pft_cleanup } -atf_test_case "tcp_in" "cleanup" -tcp_in_head() +atf_test_case "tcp_in_if_bound" "cleanup" +tcp_in_if_bound_head() { - atf_set descr 'TCP NAT64 test on inbound interface' + atf_set descr 'TCP NAT64 test on inbound interface, if-bound states' atf_set require.user root } -tcp_in_body() +tcp_in_if_bound_body() { nat64_setup_in @@ -200,7 +204,7 @@ tcp_in_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -208,21 +212,32 @@ tcp_in_body() echo "rcv=${rcv}" atf_fail "Failed to connect to TCP server" fi + + # Interfaces of the state are reversed when doing inbound NAT64! + # FIXME: Packets counters seem wrong! + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "${epair_link}a tcp 192.0.2.1:[0-9]+ \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:1234 \(64:ff9b::c000:202\[1234\]\) .* 9:9 pkts.* rule 3 .* origif: ${epair}b" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 1 ] || atf_fail "Not exactly 1 state found!" } -tcp_in_cleanup() +tcp_in_if_bound_cleanup() { pft_cleanup } -atf_test_case "tcp_out" "cleanup" -tcp_out_head() +atf_test_case "tcp_out_if_bound" "cleanup" +tcp_out_if_bound_head() { - atf_set descr 'TCP NAT64 test on outbound interface' + atf_set descr 'TCP NAT64 test on outbound interface, if-bound states' atf_set require.user root } -tcp_out_body() +tcp_out_if_bound_body() { nat64_setup_out @@ -230,7 +245,7 @@ tcp_out_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -238,9 +253,102 @@ tcp_out_body() echo "rcv=${rcv}" atf_fail "Failed to connect to TCP server" fi + + # Origif is not printed when identical as if. + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "${epair}b tcp 64:ff9b::c000:202\[1234\] <- 2001:db8::2\[[0-9]+\] .* 5:4 pkts.* rule 3 .*creatorid" \ + "${epair_link}a tcp 192.0.2.1:[0-9]+ \(64:ff9b::c000:202\[1234\]\) -> 192.0.2.2:1234 \(2001:db8::2\[[0-9]+\]\).* 5:4 pkts.* rule 4 .*creatorid" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 2 ] || atf_fail "Not exactly 2 states found!" } -tcp_out_cleanup() +tcp_out_if_bound_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_in_floating" "cleanup" +tcp_in_floating_head() +{ + atf_set descr 'TCP NAT64 test on inbound interface, floating states' + atf_set require.user root +} + +tcp_in_floating_body() +{ + nat64_setup_in "floating" + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 3 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 + + # Interfaces of the state are reversed when doing inbound NAT64! + # FIXME: Packets counters seem wrong! + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "all tcp 192.0.2.1:[0-9]+ \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:1234 \(64:ff9b::c000:202\[1234\]\).* 9:9 pkts.* rule 3 .* origif: ${epair}b" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 1 ] || atf_fail "Not exactly 1 state found!" +} + +tcp_in_floating_cleanup() +{ + pft_cleanup +} + +atf_test_case "tcp_out_floating" "cleanup" +tcp_out_floating_head() +{ + atf_set descr 'TCP NAT64 test on outbound interface, floating states' + atf_set require.user root +} + +tcp_out_floating_body() +{ + nat64_setup_out "floating" + + echo "foo" | jexec dst nc -l 1234 & + + # Sanity check & delay for nc startup + atf_check -s exit:0 -o ignore \ + ping6 -c 3 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 + + # Origif is not printed when identical as if. + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + for state_regexp in \ + "all tcp 64:ff9b::c000:202\[1234\] <- 2001:db8::2\[[0-9]+\] .* 5:4 pkts,.* rule 3 .*creatorid"\ + "all tcp 192.0.2.1:[0-9]+ \(64:ff9b::c000:202\[1234\]\) -> 192.0.2.2:1234 \(2001:db8::2\[[0-9]+\]\) .* 5:4 pkts,.* rule 4 .*creatorid"\ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + [ $(cat $states | grep tcp | wc -l) -eq 2 ] || atf_fail "Not exactly 2 states found!" +} + +tcp_out_floating_cleanup() { pft_cleanup } @@ -260,7 +368,7 @@ udp_in_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -290,7 +398,7 @@ udp_out_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -323,7 +431,7 @@ sctp_in_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -356,7 +464,7 @@ sctp_out_body() # Sanity check & delay for nc startup atf_check -s exit:0 -o ignore \ - ping6 -c 1 64:ff9b::192.0.2.2 + ping6 -c 3 64:ff9b::192.0.2.2 rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234) if [ "${rcv}" != "foo" ]; @@ -433,7 +541,9 @@ no_v4_body() 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)" + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "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 @@ -484,7 +594,9 @@ range_body() 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" + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "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 @@ -545,6 +657,8 @@ pool_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 @@ -642,6 +756,8 @@ table_range_body() "set reassemble yes" \ "set state-policy if-bound" \ "table <wanaddrs> { 192.0.2.2/31 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> round-robin" # Use pf to count sources @@ -699,6 +815,8 @@ table_common_body() "set reassemble yes" \ "set state-policy if-bound" \ "table <wanaddrs> { 192.0.2.1, 192.0.2.3, 192.0.2.4 }" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> ${pool_type}" # Use pf to count sources @@ -798,6 +916,8 @@ dummynet_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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) @@ -860,6 +980,8 @@ gateway6_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "pass in on ${epair_lan_link}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" # One ping @@ -912,10 +1034,21 @@ route_to_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 + + states=$(mktemp) || exit 1 + jexec rtr pfctl -qvvss | normalize_pfctl_s > $states + + for state_regexp in \ + "${epair}b ipv6-icmp 192.0.2.1:.* \(2001:db8::2\[[0-9]+\]\) -> 192.0.2.2:8 \(64:ff9b::c000:202\[[0-9]+\]\).*4:2 pkts.*route-to: 192.0.2.2@${epair_link}a" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done } route_to_cleanup() @@ -956,6 +1089,8 @@ reply_to_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 \ @@ -1015,6 +1150,8 @@ v6_gateway_body() pft_set_rules rtr \ "set reassemble yes" \ "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ "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 \ @@ -1034,8 +1171,10 @@ atf_init_test_cases() 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 "tcp_in_if_bound" + atf_add_test_case "tcp_out_if_bound" + atf_add_test_case "tcp_in_floating" + atf_add_test_case "tcp_out_floating" atf_add_test_case "udp_in" atf_add_test_case "udp_out" atf_add_test_case "sctp_in" diff --git a/tests/sys/netpfil/pf/nat66.py b/tests/sys/netpfil/pf/nat66.py index f93512b5b99c..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 = { diff --git a/tests/sys/netpfil/pf/pflog.sh b/tests/sys/netpfil/pf/pflog.sh index fdd9af6316d0..a34ec893a75c 100644 --- a/tests/sys/netpfil/pf/pflog.sh +++ b/tests/sys/netpfil/pf/pflog.sh @@ -238,7 +238,7 @@ state_max_body() cat pflog.txt # Second ping is blocked due to the state limit. - atf_check -o match:".*rule 0/0\(match\): block in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \ + 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 @@ -246,6 +246,18 @@ state_max_body() # 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() diff --git a/tests/sys/netpfil/pf/pfsync.sh b/tests/sys/netpfil/pf/pfsync.sh index 7f545b43a066..3be4a3024393 100644 --- a/tests/sys/netpfil/pf/pfsync.sh +++ b/tests/sys/netpfil/pf/pfsync.sh @@ -835,6 +835,90 @@ basic_ipv6_cleanup() pfsynct_cleanup } +atf_test_case "rtable" "cleanup" +rtable_head() +{ + atf_set descr 'Test handling of invalid rtableid' + atf_set require.user root +} + +rtable_body() +{ + pfsynct_init + + epair_sync=$(vnet_mkepair) + epair_one=$(vnet_mkepair) + epair_two=$(vnet_mkepair) + + vnet_mkjail one ${epair_one}a ${epair_sync}a + vnet_mkjail two ${epair_two}a ${epair_sync}b + + # pfsync interface + jexec one ifconfig ${epair_sync}a 192.0.2.1/24 up + jexec one ifconfig ${epair_one}a 198.51.100.1/24 up + jexec one ifconfig pfsync0 \ + syncdev ${epair_sync}a \ + maxupd 1 \ + up + jexec two ifconfig ${epair_two}a 198.51.100.1/24 up + jexec two ifconfig ${epair_sync}b 192.0.2.2/24 up + jexec two ifconfig pfsync0 \ + syncdev ${epair_sync}b \ + maxupd 1 \ + up + + # Make life easy, give ${epair_two}a the same mac addrss as ${epair_one}a + mac=$(jexec one ifconfig ${epair_one}a | awk '/ether/ { print($2); }') + jexec two ifconfig ${epair_two}a ether ${mac} + + # Enable pf! + jexec one /sbin/sysctl net.fibs=8 + jexec one pfctl -e + pft_set_rules one \ + "set skip on ${epair_sync}a" \ + "pass rtable 3 keep state" + # No extra fibs in two + jexec two pfctl -e + pft_set_rules two \ + "set skip on ${epair_sync}b" \ + "pass keep state" + + ifconfig ${epair_one}b 198.51.100.254/24 up + ifconfig ${epair_two}b 198.51.100.253/24 up + + # Create a new state + env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_one}b \ + --fromaddr 198.51.100.254 \ + --to 198.51.100.1 \ + --recvif ${epair_one}b + + # Now + jexec one pfctl -ss -vv + sleep 2 + + # Now try to use that state on jail two + env PYTHONPATH=${common_dir} \ + ${common_dir}/pft_ping.py \ + --sendif ${epair_two}b \ + --fromaddr 198.51.100.254 \ + --to 198.51.100.1 \ + --recvif ${epair_two}b + + echo one + jexec one pfctl -ss -vv + jexec one pfctl -sr -vv + echo two + jexec two pfctl -ss -vv + jexec two pfctl -sr -vv +} + +rtable_cleanup() +{ + pfsynct_cleanup +} + route_to_common_head() { pfsync_version=$1 @@ -1134,6 +1218,7 @@ 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 "rtable" 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" diff --git a/tests/sys/netpfil/pf/rdr.sh b/tests/sys/netpfil/pf/rdr.sh index a7a8c77c0515..f7c920bbfa8f 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 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,9 +75,11 @@ 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 \ @@ -116,19 +110,44 @@ tcp_v6_body() atf_check_equal " 0" "$count" } -tcp_v6_cleanup() +atf_test_case "tcp_v6_compat" "cleanup" +tcp_v6_compat_head() { - pft_cleanup + 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" +} -atf_test_case "srcport" "cleanup" -srcport_head() +tcp_v6_compat_cleanup() { - atf_set descr 'TCP rdr srcport modulation' + 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 - atf_set timeout 9999 +} + +tcp_v6_pass_body() +{ + tcp_v6_setup # Sets ${epair_…} variables + tcp_v6_common \ + "pass in on ${epair_one}a proto tcp from any to any port 80 rdr-to 2001:db8:b::2 port 8000" +} + +tcp_v6_pass_cleanup() +{ + pft_cleanup } # @@ -145,7 +164,7 @@ srcport_head() # In this case, the rdr rule should also rewrite the source port (again) to # resolve the state conflict. # -srcport_body() +srcport_setup() { pft_init @@ -188,14 +207,17 @@ srcport_body() 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" \ - "nat on ${epair2}a inet from 198.51.100.0/24 to any -> ${epair2}a static-port" + "${1}" pft_set_rules ${j}c \ "set debug misc" \ - "rdr on ${epair2}b proto tcp from any to ${epair2}b port 7777 -> 203.0.113.50 port 8888" + "${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 @@ -215,13 +237,54 @@ srcport_body() atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port3 } -srcport_cleanup() +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 "srcport" + 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/route_to.sh b/tests/sys/netpfil/pf/route_to.sh index 0354d1f59306..765403dcb79c 100644 --- a/tests/sys/netpfil/pf/route_to.sh +++ b/tests/sys/netpfil/pf/route_to.sh @@ -813,6 +813,169 @@ 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_test_case "empty_pool" "cleanup" +empty_pool_head() +{ + atf_set descr 'Route-to with empty pool' + atf_set require.user root + atf_set require.progs python3 scapy +} + +empty_pool_body() +{ + pft_init + setup_router_server_ipv6 + + + pft_set_rules router \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b route-to (${epair_server}a <nonexistent>) inet6 from any to ${net_server_host_server}" \ + "pass out on ${epair_server}a" + + # pf_map_addr_sn() won't be able to pick a target address, because + # the table used in redireciton pool is empty. Packet will not be + # forwarded, error counter will be increased. + ping_server_check_reply exit:1 + # Ignore warnings about not-loaded ALTQ + atf_check -o "match:map-failed +1 +" -x "jexec router pfctl -qvvsi 2> /dev/null" +} + +empty_pool_cleanup() +{ + pft_cleanup +} + + +atf_test_case "table_loop" "cleanup" + +table_loop_head() +{ + atf_set descr 'Check that iterating over tables poperly loops' + atf_set require.user root + atf_set require.progs python3 scapy +} + +table_loop_body() +{ + setup_router_server_nat64 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 ${net_clients_6}::/${net_clients_6_mask} ${net_tester_6_host_tester} + jexec router route add ${net_clients_4}.0/${net_clients_4_mask} ${net_tester_4_host_tester} + + # The servers are reachable over additional IP addresses for + # testing of tables and subnets. The addresses are noncontinougnus + # for pf_map_addr() counter tests. + for i in 0 1 4 5; do + a1=$((24 + i)) + jexec server1 ifconfig ${epair_server1}b inet ${net_server1_4}.${a1}/32 alias + jexec server1 ifconfig ${epair_server1}b inet6 ${net_server1_6}::42:${i}/128 alias + a2=$((40 + i)) + jexec server2 ifconfig ${epair_server2}b inet ${net_server2_4}.${a2}/32 alias + jexec server2 ifconfig ${epair_server2}b inet6 ${net_server2_6}::42:${i}/128 alias + done + + jexec router pfctl -e + pft_set_rules router \ + "set debug loud" \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "table <rt_targets_1> { ${net_server1_6}::42:4/127 ${net_server1_6}::42:0/127 }" \ + "table <rt_targets_2> { ${net_server2_6}::42:4/127 }" \ + "pass in on ${epair_tester}b \ + route-to { \ + (${epair_server1}a <rt_targets_1>) \ + (${epair_server2}a <rt_targets_2_empty>) \ + (${epair_server2}a <rt_targets_2>) \ + } \ + inet6 proto tcp \ + keep state" + + # Both hosts of the pool are tables. Each table gets iterated over once, + # then the pool iterates to the next host, which is also iterated, + # then the pool loops back to the 1st host. If an empty table is found, + # it is skipped. Unless that's the only table, that is tested by + # the "empty_pool" test. + for port in $(seq 1 7); do + port=$((4200 + port)) + atf_check -s exit:0 ${common_dir}/pft_ping.py \ + --sendif ${epair_tester}a --replyif ${epair_tester}a \ + --fromaddr ${net_clients_6}::1 --to ${host_server_6} \ + --ping-type=tcp3way --send-sport=${port} + done + + states=$(mktemp) || exit 1 + jexec router pfctl -qvvss | normalize_pfctl_s > $states + cat $states + + for state_regexp in \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4201\] .* route-to: ${net_server1_6}::42:0@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4202\] .* route-to: ${net_server1_6}::42:1@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4203\] .* route-to: ${net_server1_6}::42:4@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4204\] .* route-to: ${net_server1_6}::42:5@${epair_server1}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4205\] .* route-to: ${net_server2_6}::42:4@${epair_server2}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4206\] .* route-to: ${net_server2_6}::42:5@${epair_server2}a" \ + "${epair_tester}b tcp ${host_server_6}\[9\] <- ${net_clients_6}::1\[4207\] .* route-to: ${net_server1_6}::42:0@${epair_server1}a" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done +} + +table_loop_cleanup() +{ + pft_cleanup +} + + atf_init_test_cases() { atf_add_test_case "v4" @@ -830,4 +993,7 @@ atf_init_test_cases() atf_add_test_case "dummynet_frag" atf_add_test_case "dummynet_double" atf_add_test_case "sticky" + atf_add_test_case "ttl" + atf_add_test_case "empty_pool" + atf_add_test_case "table_loop" } diff --git a/tests/sys/netpfil/pf/sctp.py b/tests/sys/netpfil/pf/sctp.py index 230dbae0d327..f492f26b63a1 100644 --- a/tests/sys/netpfil/pf/sctp.py +++ b/tests/sys/netpfil/pf/sctp.py @@ -271,6 +271,9 @@ class TestSCTP(VnetTestTemplate): "pass inet proto sctp to 192.0.2.0/24", "pass on lo"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) client.send(b"hello", 0) @@ -309,6 +312,9 @@ class TestSCTP(VnetTestTemplate): "pass on lo", "pass inet proto sctp from 192.0.2.0/24"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234, "192.0.2.1") client.send(b"hello", 0) @@ -379,6 +385,9 @@ class TestSCTP(VnetTestTemplate): "pass on lo", "pass inet proto sctp to 192.0.2.0/24"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) client.send(b"hello", 0) @@ -410,6 +419,9 @@ class TestSCTP(VnetTestTemplate): "pass on lo", "pass inet proto sctp to 192.0.2.0/24"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) client.send(b"hello", 0) @@ -427,6 +439,37 @@ class TestSCTP(VnetTestTemplate): 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"]) + + # Give the server some time to come up + time.sleep(3) + + # 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"] @@ -436,6 +479,9 @@ class TestSCTP(VnetTestTemplate): "pass inet proto sctp to 192.0.2.3", "pass on lo"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) client.send(b"hello", 0) @@ -474,6 +520,9 @@ class TestSCTP(VnetTestTemplate): "pass inet proto sctp to 192.0.2.3 keep state (allow-related)", "pass on lo"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("192.0.2.3", 1234) client.send(b"hello", 0) @@ -530,6 +579,9 @@ class TestSCTPv6(VnetTestTemplate): "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("2001:db8::3", 1234) client.send(b"hello", 0) @@ -568,6 +620,9 @@ class TestSCTPv6(VnetTestTemplate): "pass on lo", "pass inet6 proto sctp from 2001:db8::/64"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("2001:db8::3", 1234, "2001:db8::1") client.send(b"hello", 0) @@ -637,6 +692,9 @@ class TestSCTPv6(VnetTestTemplate): "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("2001:db8::3", 1234) client.send(b"hello", 0) @@ -668,6 +726,9 @@ class TestSCTPv6(VnetTestTemplate): "pass on lo", "pass inet6 proto sctp to 2001:db8::0/64"]) + # Give the server some time to come up + time.sleep(3) + # Sanity check, we can communicate with the primary address. client = SCTPClient("2001:db8::3", 1234) client.send(b"hello", 0) diff --git a/tests/sys/netpfil/pf/sctp.sh b/tests/sys/netpfil/pf/sctp.sh index 563103827fac..57dcdad1d866 100644 --- a/tests/sys/netpfil/pf/sctp.sh +++ b/tests/sys/netpfil/pf/sctp.sh @@ -810,7 +810,7 @@ related_icmp_body() fi # Do we see ICMP traffic if we send overly large traffic? - echo "foo" | jexec srv nc --sctp -N -l 1234 >/dev/null & + echo "foo" | jexec srv nc --sctp -l 1234 >/dev/null & sleep 1 atf_check -s exit:0 -o not-match:".*destination unreachable:.*" \ @@ -818,10 +818,10 @@ related_icmp_body() # 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=1600 count=1 | nc --sctp -N -w 3 203.0.113.2 1234 + 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" \ + atf_check -s exit:0 -o match:".*destination unreachable: [1-9]" \ netstat -s -p icmp } diff --git a/tests/sys/netpfil/pf/set_tos.sh b/tests/sys/netpfil/pf/set_tos.sh index 75b96edbab6e..842377ee97c6 100644 --- a/tests/sys/netpfil/pf/set_tos.sh +++ b/tests/sys/netpfil/pf/set_tos.sh @@ -129,10 +129,6 @@ v6_body() { pft_init - if [ "$(atf_config_get ci false)" = "true" ]; then - atf_skip "https://bugs.freebsd.org/260459" - fi - epair=$(vnet_mkepair) ifconfig ${epair}a inet6 add 2001:db8:192::1 vnet_mkjail alcatraz ${epair}b diff --git a/tests/sys/netpfil/pf/src_track.sh b/tests/sys/netpfil/pf/src_track.sh index 3668898682ff..ae60a5df809b 100755 --- a/tests/sys/netpfil/pf/src_track.sh +++ b/tests/sys/netpfil/pf/src_track.sh @@ -307,14 +307,14 @@ max_src_states_global_cleanup() pft_cleanup } -route_to_head() +sn_types_compat_head() { - atf_set descr 'Max states per source per rule with route-to' + atf_set descr 'Combination of source node types with compat NAT rules' atf_set require.user root atf_set require.progs python3 scapy } -route_to_body() +sn_types_compat_body() { setup_router_dummy_ipv6 @@ -398,11 +398,173 @@ route_to_body() ! grep -q 'filter rule 3' $nodes || atf_fail "Source node found for rule 3" } -route_to_cleanup() +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_test_case "mixed_af" "cleanup" +mixed_af_head() +{ + atf_set descr 'Test mixed address family source tracking' + atf_set require.user root + atf_set require.progs python3 scapy +} + +mixed_af_body() +{ + setup_router_server_nat64 + + # Clients will connect from another network behind the router. + # This allows for using multiple source addresses. + jexec router route add -6 ${net_clients_6}::/${net_clients_6_mask} ${net_tester_6_host_tester} + + jexec router pfctl -e + pft_set_rules router \ + "set reassemble yes" \ + "set state-policy if-bound" \ + "block" \ + "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \ + "pass in on ${epair_tester}b \ + route-to { (${epair_server1}a ${net_server1_4_host_server}) \ + } sticky-address \ + inet6 proto tcp from any to 64:ff9b::/96 \ + af-to inet from ${net_clients_4}.0/${net_clients_4_mask} round-robin sticky-address" + + atf_check -s exit:0 ${common_dir}/pft_ping.py \ + --sendif ${epair_tester}a \ + --replyif ${epair_tester}a \ + --fromaddr 2001:db8:44::1 \ + --to 64:ff9b::192.0.2.100 \ + --ping-type=tcp3way \ + --send-sport=4201 + + states=$(mktemp) || exit 1 + jexec router pfctl -qvvss | normalize_pfctl_s > $states + nodes=$(mktemp) || exit 1 + jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes + + # States are checked for proper route-to information. + # The route-to gateway is IPv4. + for state_regexp in \ + "${epair_tester}b tcp 203.0.113.0:4201 \(2001:db8:44::1\[4201\]\) -> 192.0.2.100:9 \(64:ff9b::c000:264\[9\]\) .* route-to: 198.51.100.18@${epair_server1}a" \ + ; do + grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'" + done + + # Source nodes map IPv6 source address onto IPv4 gateway and IPv4 SNAT address. + for node_regexp in \ + '2001:db8:44::1 -> 203.0.113.0 .* states 1, .* NAT/RDR sticky-address' \ + '2001:db8:44::1 -> 198.51.100.18 .* states 1, .* route sticky-address' \ + ; do + grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'" + done +} + +mixed_af_cleanup() +{ + pft_cleanup +} atf_init_test_cases() { @@ -411,5 +573,7 @@ atf_init_test_cases() 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 "route_to" + atf_add_test_case "sn_types_compat" + atf_add_test_case "sn_types_pass" + atf_add_test_case "mixed_af" } diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh index 78320375db7c..65492545a13b 100644 --- a/tests/sys/netpfil/pf/table.sh +++ b/tests/sys/netpfil/pf/table.sh @@ -582,6 +582,97 @@ anchor_cleanup() pft_cleanup } +atf_test_case "flush" "cleanup" +flush_head() +{ + atf_set descr 'Test flushing addresses from tables' + atf_set require.user root +} + +flush_body() +{ + pft_init + + vnet_mkjail alcatraz + + atf_check -s exit:0 -e match:"1/1 addresses added." \ + jexec alcatraz pfctl -t foo -T add 1.2.3.4 + atf_check -s exit:0 -o match:" 1.2.3.4" \ + jexec alcatraz pfctl -t foo -T show + atf_check -s exit:0 -e match:"1 addresses deleted." \ + jexec alcatraz pfctl -t foo -T flush + atf_check -s exit:0 -o not-match:"1.2.3.4" \ + jexec alcatraz pfctl -t foo -T show +} + +flush_cleanup() +{ + pft_cleanup +} + +atf_test_case "large" "cleanup" +large_head() +{ + atf_set descr 'Test loading a large list of addresses' + atf_set require.user root +} + +large_body() +{ + pft_init + pwd=$(pwd) + + vnet_mkjail alcatraz + + for i in `seq 1 255`; do + for j in `seq 1 255`; do + echo "1.2.${i}.${j}" >> ${pwd}/foo.lst + done + done + expected=$(wc -l foo.lst | awk '{ print $1; }') + + jexec alcatraz pfctl -e + pft_set_rules alcatraz \ + "table <foo>" \ + "pass in from <foo>" \ + "pass" + + atf_check -s exit:0 \ + -e match:"${expected}/${expected} addresses added." \ + jexec alcatraz pfctl -t foo -T add -f ${pwd}/foo.lst + actual=$(jexec alcatraz pfctl -t foo -T show | wc -l | awk '{ print $1; }') + if [ $actual -ne $expected ]; then + atf_fail "Unexpected number of table entries $expected $acual" + fi + + # The second pass should work too, but confirm we've inserted everything + atf_check -s exit:0 \ + -e match:"0/${expected} addresses added." \ + jexec alcatraz pfctl -t foo -T add -f ${pwd}/foo.lst + + echo '42.42.42.42' >> ${pwd}/foo.lst + expected=$((${expected} + 1)) + + # And we can also insert one additional address + atf_check -s exit:0 \ + -e match:"1/${expected} addresses added." \ + jexec alcatraz pfctl -t foo -T add -f ${pwd}/foo.lst + + # Try to delete one address + atf_check -s exit:0 \ + -e match:"1/1 addresses deleted." \ + jexec alcatraz pfctl -t foo -T delete 42.42.42.42 + # And again, for the same address + atf_check -s exit:0 \ + -e match:"0/1 addresses deleted." \ + jexec alcatraz pfctl -t foo -T delete 42.42.42.42 +} + +large_cleanup() +{ + pft_cleanup +} + atf_init_test_cases() { atf_add_test_case "v4_counters" @@ -596,4 +687,6 @@ atf_init_test_cases() atf_add_test_case "pr259689" atf_add_test_case "precreate" atf_add_test_case "anchor" + atf_add_test_case "flush" + atf_add_test_case "large" } 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/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 6af10e80390d..3f8d437920f9 100644 --- a/tests/sys/netpfil/pf/utils.subr +++ b/tests/sys/netpfil/pf/utils.subr @@ -274,6 +274,107 @@ setup_router_server_ipv6() jexec server inetd -p ${PWD}/inetd.pid $inetd_conf } +# Create a router and 2 server jails for nat64 and rfc5549 test cases. +# The router is connected to servers, both are dual-stack, and to the +# tester jail. All links are dual stack. +setup_router_server_nat64() +{ + pft_init + + epair_tester=$(vnet_mkepair) + epair_server1=$(vnet_mkepair) + epair_server2=$(vnet_mkepair) + + # Funny how IPv4 address space is to small to even assign nice /24 + # prefixes on all needed networks. On IPv6 we have a separate /64 for + # each link, loopback server, and client/SNAT pool. On IPv4 we must + # use small /28 prefixes, so even though we define all networks + # as variables we can't easily use them in tests if additional addresses + # are needed. + + # IP addresses which can be used by the tester jail. + # Can be used as SNAT or as source with pft_ping.py. It is up to + # the test code to make them accessible from router. + net_clients_4=203.0.113 + net_clients_4_mask=24 + net_clients_6=2001:db8:44 + net_clients_6_mask=64 + + # IP addresses on loopback interfaces of both servers. They can be + # accessed using the route-to targtet. + host_server_4=192.0.2.100 + host_server_6=2001:db8:4203::100 + + net_tester_4=198.51.100 + net_tester_4_mask=28 + net_tester_4_host_router=198.51.100.1 + net_tester_4_host_tester=198.51.100.2 + + net_tester_6=2001:db8:4200 + net_tester_6_mask=64 + net_tester_6_host_router=2001:db8:4200::1 + net_tester_6_host_tester=2001:db8:4200::2 + + net_server1_4=198.51.100 + net_server1_4_mask=28 + net_server1_4_host_router=198.51.100.17 + net_server1_4_host_server=198.51.100.18 + + net_server1_6=2001:db8:4201 + net_server1_6_mask=64 + net_server1_6_host_router=2001:db8:4201::1 + net_server1_6_host_server=2001:db8:4201::2 + + net_server2_4=198.51.100 + net_server2_4_mask=28 + net_server2_4_host_router=198.51.100.33 + net_server2_4_host_server=198.51.100.34 + + net_server2_6=2001:db8:4202 + net_server2_6_mask=64 + net_server2_6_host_router=2001:db8:4202::1 + net_server2_6_host_server=2001:db8:4202::2 + + vnet_mkjail router ${epair_tester}b ${epair_server1}a ${epair_server2}a + jexec router ifconfig ${epair_tester}b inet ${net_tester_4_host_router}/${net_tester_4_mask} up + jexec router ifconfig ${epair_tester}b inet6 ${net_tester_6_host_router}/${net_tester_6_mask} up no_dad + jexec router ifconfig ${epair_server1}a inet ${net_server1_4_host_router}/${net_server1_4_mask} up + jexec router ifconfig ${epair_server1}a inet6 ${net_server1_6_host_router}/${net_server1_6_mask} up no_dad + jexec router ifconfig ${epair_server2}a inet ${net_server2_4_host_router}/${net_server2_4_mask} up + jexec router ifconfig ${epair_server2}a inet6 ${net_server2_6_host_router}/${net_server2_6_mask} up no_dad + jexec router sysctl net.inet.ip.forwarding=1 + jexec router sysctl net.inet6.ip6.forwarding=1 + jexec router pfctl -e + + ifconfig ${epair_tester}a inet ${net_tester_4_host_tester}/${net_tester_4_mask} up + ifconfig ${epair_tester}a inet6 ${net_tester_6_host_tester}/${net_tester_6_mask} up no_dad + route add 0.0.0.0/0 ${net_tester_4_host_router} + route add -6 ::/0 ${net_tester_6_host_router} + + inetd_conf=$(mktemp) + echo "discard stream tcp46 nowait root internal" >> $inetd_conf + + vnet_mkjail server1 ${epair_server1}b + jexec server1 /etc/rc.d/netif start lo0 + jexec server1 ifconfig ${epair_server1}b inet ${net_server1_4_host_server}/${net_server1_4_mask} up + jexec server1 ifconfig ${epair_server1}b inet6 ${net_server1_6_host_server}/${net_server1_6_mask} up no_dad + jexec server1 ifconfig lo0 ${host_server_4}/32 alias + jexec server1 ifconfig lo0 inet6 ${host_server_6}/128 alias + jexec server1 inetd -p ${PWD}/inetd_1.pid $inetd_conf + jexec server1 route add 0.0.0.0/0 ${net_server1_4_host_router} + + jexec server1 route add -6 ::/0 ${net_server1_6_host_router} + vnet_mkjail server2 ${epair_server2}b + jexec server2 /etc/rc.d/netif start lo0 + jexec server2 ifconfig ${epair_server2}b inet ${net_server2_4_host_server}/${net_server2_4_mask} up + jexec server2 ifconfig ${epair_server2}b inet6 ${net_server2_6_host_server}/${net_server2_6_mask} up no_dad + jexec server2 ifconfig lo0 ${host_server_4}/32 alias + jexec server2 ifconfig lo0 inet6 ${host_server_6}/128 alias + jexec server2 inetd -p ${PWD}/inetd_2.pid $inetd_conf + jexec server2 route add 0.0.0.0/0 ${net_server2_4_host_router} + jexec server2 route add -6 ::/0 ${net_server2_6_host_router} +} + # Ping the dummy static NDP target. # Check for pings being forwarded through the router towards the target. ping_dummy_check_request() diff --git a/tests/sys/sound/sndstat.c b/tests/sys/sound/sndstat.c index 553c745ec950..ed292b570429 100644 --- a/tests/sys/sound/sndstat.c +++ b/tests/sys/sound/sndstat.c @@ -148,10 +148,10 @@ ATF_TC_BODY(sndstat_nv, tc) NV(number, UNIT); NV(string, STATUS); NV(bool, BITPERFECT); - NV(number, PVCHAN); + NV(bool, PVCHAN); NV(number, PVCHANRATE); NV(number, PVCHANFORMAT); - NV(number, RVCHAN); + NV(bool, RVCHAN); NV(number, PVCHANRATE); NV(number, PVCHANFORMAT); #undef NV @@ -184,12 +184,14 @@ ATF_TC_BODY(sndstat_nv, tc) 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); diff --git a/tests/sys/sys/Makefile b/tests/sys/sys/Makefile index 40060911856f..a1b4e3234e1c 100644 --- a/tests/sys/sys/Makefile +++ b/tests/sys/sys/Makefile @@ -7,6 +7,7 @@ ATF_TESTS_C= arb_test \ bitstring_test \ buf_ring_test \ qmath_test \ + queue_test \ rb_test \ splay_test \ time_test 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/vm/soxstack/Makefile b/tests/sys/vm/soxstack/Makefile index bd159c2fde75..f9f3bd55b50a 100644 --- a/tests/sys/vm/soxstack/Makefile +++ b/tests/sys/vm/soxstack/Makefile @@ -1,3 +1,4 @@ +PACKAGE= tests SHLIB= soxstack SHLIB_NAME= libsoxstack.so SHLIB_MAJOR= 1 |