aboutsummaryrefslogtreecommitdiff
path: root/tests/sys
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys')
-rw-r--r--tests/sys/cam/ctl/Makefile6
-rw-r--r--tests/sys/cam/ctl/ctl.subr27
-rw-r--r--tests/sys/cam/ctl/persist.sh349
-rw-r--r--tests/sys/cam/ctl/prout_register_huge_cdb.c88
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/Makefile2
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh4
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_001_neg.ksh64
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd_offline_002_neg.ksh66
-rwxr-xr-xtests/sys/cddl/zfs/tests/zfsd/zfsd_test.sh60
-rw-r--r--tests/sys/file/closefrom_test.c35
-rw-r--r--tests/sys/file/dup_test.c98
-rw-r--r--tests/sys/fs/fusefs/Makefile14
-rw-r--r--tests/sys/fs/fusefs/ctl.sh69
-rw-r--r--tests/sys/fs/fusefs/destroy.cc2
-rw-r--r--tests/sys/fs/fusefs/fallocate.cc3
-rw-r--r--tests/sys/fs/fusefs/flush.cc30
-rw-r--r--tests/sys/fs/fusefs/last_local_modify.cc5
-rw-r--r--tests/sys/fs/fusefs/lookup.cc7
-rw-r--r--tests/sys/fs/fusefs/mockfs.cc23
-rw-r--r--tests/sys/fs/fusefs/mockfs.hh6
-rw-r--r--tests/sys/fs/fusefs/mount.cc2
-rw-r--r--tests/sys/fs/fusefs/nfs.cc106
-rw-r--r--tests/sys/fs/fusefs/pre-init.cc226
-rw-r--r--tests/sys/fs/fusefs/utils.cc12
-rw-r--r--tests/sys/fs/fusefs/utils.hh2
-rw-r--r--tests/sys/fs/fusefs/xattr.cc50
-rw-r--r--tests/sys/fs/tarfs/tarfs_test.sh18
-rw-r--r--tests/sys/kern/Makefile13
-rw-r--r--tests/sys/kern/copy_file_range.c231
-rw-r--r--tests/sys/kern/exterr_test.c108
-rw-r--r--tests/sys/kern/getdirentries_test.c172
-rw-r--r--tests/sys/kern/inotify_test.c864
-rw-r--r--tests/sys/kern/jail_lookup_root.c133
-rw-r--r--tests/sys/kern/ptrace_test.c141
-rw-r--r--tests/sys/kern/socket_splice.c4
-rw-r--r--tests/sys/kern/tty/Makefile3
-rw-r--r--tests/sys/kern/tty/test_sti.c337
-rw-r--r--tests/sys/kern/unix_passfd_test.c205
-rw-r--r--tests/sys/kern/unix_seqpacket_test.c36
-rw-r--r--tests/sys/kern/unix_stream.c381
-rw-r--r--tests/sys/kqueue/libkqueue/timer.c2
-rw-r--r--tests/sys/mac/bsdextended/Makefile1
-rw-r--r--tests/sys/mac/bsdextended/matches_test.sh3
-rw-r--r--tests/sys/mac/portacl/Makefile1
-rw-r--r--tests/sys/net/Makefile5
-rw-r--r--tests/sys/net/bpf/Makefile15
-rw-r--r--tests/sys/net/bpf/bpf.sh67
-rw-r--r--tests/sys/net/bpf/bpf_multi_read.c76
-rwxr-xr-xtests/sys/net/if_bridge_test.sh747
-rw-r--r--tests/sys/net/if_gif.sh301
-rwxr-xr-xtests/sys/net/if_lagg_test.sh4
-rw-r--r--tests/sys/net/if_ovpn/if_ovpn.sh334
-rwxr-xr-xtests/sys/net/if_tun_test.sh22
-rwxr-xr-xtests/sys/net/if_vlan.sh27
-rw-r--r--tests/sys/net/if_wg.sh220
-rw-r--r--tests/sys/net/transient_tuntap.c54
-rw-r--r--tests/sys/netgraph/ksocket.c99
-rwxr-xr-xtests/sys/netinet/arp.sh8
-rw-r--r--tests/sys/netinet/broadcast.c6
-rw-r--r--tests/sys/netinet/fibs_test.sh3
-rw-r--r--tests/sys/netinet/igmp.py50
-rw-r--r--tests/sys/netinet/ip_reass_test.c12
-rw-r--r--tests/sys/netinet/so_reuseport_lb_test.c10
-rw-r--r--tests/sys/netinet/socket_afinet.c3
-rw-r--r--tests/sys/netinet/tcp_implied_connect.c1
-rw-r--r--tests/sys/netinet/udp_io.c1
-rw-r--r--tests/sys/netinet6/Makefile5
-rwxr-xr-xtests/sys/netinet6/addr6.sh70
-rw-r--r--tests/sys/netinet6/redirect.sh6
-rw-r--r--tests/sys/netlink/test_snl.c12
-rw-r--r--tests/sys/netlink/test_snl_generic.c4
-rw-r--r--tests/sys/netpfil/common/dummynet.sh4
-rw-r--r--tests/sys/netpfil/pf/Makefile11
-rw-r--r--tests/sys/netpfil/pf/anchor.sh139
-rw-r--r--tests/sys/netpfil/pf/debug.sh50
-rw-r--r--tests/sys/netpfil/pf/forward.sh4
-rw-r--r--tests/sys/netpfil/pf/frag4.py72
-rw-r--r--tests/sys/netpfil/pf/frag6.py161
-rw-r--r--tests/sys/netpfil/pf/header.py216
-rw-r--r--tests/sys/netpfil/pf/icmp.py106
-rw-r--r--tests/sys/netpfil/pf/igmp.py95
-rw-r--r--tests/sys/netpfil/pf/ioctl/validation.c35
-rw-r--r--tests/sys/netpfil/pf/killstate.sh61
-rw-r--r--tests/sys/netpfil/pf/limits.sh53
-rw-r--r--tests/sys/netpfil/pf/map_e.sh90
-rw-r--r--tests/sys/netpfil/pf/max_pkt_rate.sh121
-rw-r--r--tests/sys/netpfil/pf/max_pkt_size.sh122
-rw-r--r--tests/sys/netpfil/pf/mbuf.sh32
-rw-r--r--tests/sys/netpfil/pf/mld.py95
-rw-r--r--tests/sys/netpfil/pf/nat.sh408
-rw-r--r--tests/sys/netpfil/pf/nat64.py115
-rw-r--r--tests/sys/netpfil/pf/nat64.sh191
-rw-r--r--tests/sys/netpfil/pf/nat66.py15
-rw-r--r--tests/sys/netpfil/pf/pflog.sh14
-rw-r--r--tests/sys/netpfil/pf/pfsync.sh85
-rw-r--r--tests/sys/netpfil/pf/rdr.sh109
-rw-r--r--tests/sys/netpfil/pf/route_to.sh166
-rw-r--r--tests/sys/netpfil/pf/sctp.py61
-rw-r--r--tests/sys/netpfil/pf/sctp.sh6
-rw-r--r--tests/sys/netpfil/pf/set_tos.sh4
-rwxr-xr-xtests/sys/netpfil/pf/src_track.sh174
-rw-r--r--tests/sys/netpfil/pf/table.sh93
-rw-r--r--tests/sys/netpfil/pf/tcp.py158
-rw-r--r--tests/sys/netpfil/pf/utils.py46
-rw-r--r--tests/sys/netpfil/pf/utils.subr101
-rw-r--r--tests/sys/sound/sndstat.c6
-rw-r--r--tests/sys/sys/Makefile1
-rw-r--r--tests/sys/sys/queue_test.c293
-rw-r--r--tests/sys/vm/soxstack/Makefile1
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