aboutsummaryrefslogtreecommitdiff
path: root/tests/sys
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys')
-rw-r--r--tests/sys/Makefile3
-rw-r--r--tests/sys/Makefile.inc1
-rw-r--r--tests/sys/acl/Makefile1
-rw-r--r--tests/sys/aio/Makefile1
-rw-r--r--tests/sys/aio/aio_kqueue_test.c2
-rw-r--r--tests/sys/audit/Makefile1
-rw-r--r--tests/sys/auditpipe/Makefile1
-rw-r--r--tests/sys/cam/Makefile7
-rw-r--r--tests/sys/cam/ctl/Makefile20
-rw-r--r--tests/sys/cam/ctl/ctl.subr115
-rw-r--r--tests/sys/cam/ctl/persist.sh349
-rw-r--r--tests/sys/cam/ctl/prevent.sh161
-rw-r--r--tests/sys/cam/ctl/prout_register_huge_cdb.c88
-rw-r--r--tests/sys/cam/ctl/read_buffer.sh172
-rw-r--r--tests/sys/cam/ctl/start_stop_unit.sh150
-rw-r--r--tests/sys/capsicum/Makefile5
-rw-r--r--tests/sys/cddl/Makefile3
-rw-r--r--tests/sys/cddl/zfs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/bin/Makefile1
-rw-r--r--tests/sys/cddl/zfs/include/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/Makefile.inc1
-rw-r--r--tests/sys/cddl/zfs/tests/acl/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/acl/cifs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/acl/trivial/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/atime/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/bootfs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cache/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cachefile/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/clean_mirror/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_user/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_user/misc/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/compression/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/ctime/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/delegate/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/devices/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/exec/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/grow_pool/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/grow_replicas/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/history/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/hotplug/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/hotspare/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/inheritance/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/interop/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/inuse/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/iscsi/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/large_files/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/largest_pool/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/link_count/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/migration/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/mmap/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/mount/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/mv_files/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/nestedfs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/no_space/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/online_offline/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/pool_names/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/poolversion/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/quota/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/redundancy/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/refquota/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/refreserv/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/rename_dirs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/replacement/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/reservation/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/rootpool/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/rsend/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/scrub_mirror/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/slog/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/snapshot/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/snapused/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/sparse/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/threadsappend/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/truncate/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/txg_integrity/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/userquota/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/utils_test/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/write_dirs/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/xattr/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib2
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh26
-rw-r--r--tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh4
-rw-r--r--tests/sys/cddl/zfs/tests/zil/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zinject/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zones/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile1
-rw-r--r--tests/sys/cddl/zfs/tests/zvol_thrash/Makefile1
-rw-r--r--tests/sys/common/Makefile1
-rw-r--r--tests/sys/common/vnet.subr28
-rw-r--r--tests/sys/compat32/Makefile1
-rw-r--r--tests/sys/compat32/Makefile.inc1
-rw-r--r--tests/sys/devrandom/Makefile1
-rw-r--r--tests/sys/fifo/Makefile1
-rw-r--r--tests/sys/file/Makefile1
-rw-r--r--tests/sys/file/path_test.c8
-rw-r--r--tests/sys/fs/Makefile1
-rw-r--r--tests/sys/fs/Makefile.inc1
-rw-r--r--tests/sys/fs/fusefs/Makefile13
-rw-r--r--tests/sys/fs/fusefs/allow_other.cc3
-rw-r--r--tests/sys/fs/fusefs/bmap.cc25
-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.cc54
-rw-r--r--tests/sys/fs/fusefs/flush.cc30
-rw-r--r--tests/sys/fs/fusefs/forget.cc1
-rw-r--r--tests/sys/fs/fusefs/io.cc3
-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/lseek.cc140
-rw-r--r--tests/sys/fs/fusefs/mknod.cc2
-rw-r--r--tests/sys/fs/fusefs/mockfs.cc76
-rw-r--r--tests/sys/fs/fusefs/mockfs.hh20
-rw-r--r--tests/sys/fs/fusefs/mount.cc2
-rw-r--r--tests/sys/fs/fusefs/nfs.cc136
-rw-r--r--tests/sys/fs/fusefs/notify.cc1
-rw-r--r--tests/sys/fs/fusefs/open.cc40
-rw-r--r--tests/sys/fs/fusefs/opendir.cc27
-rw-r--r--tests/sys/fs/fusefs/pre-init.cc154
-rw-r--r--tests/sys/fs/fusefs/read.cc55
-rw-r--r--tests/sys/fs/fusefs/unlink.cc1
-rw-r--r--tests/sys/fs/fusefs/utils.cc27
-rw-r--r--tests/sys/fs/fusefs/utils.hh6
-rw-r--r--tests/sys/fs/fusefs/write.cc7
-rw-r--r--tests/sys/fs/fusefs/xattr.cc52
-rw-r--r--tests/sys/fs/tarfs/tarfs_test.sh18
-rw-r--r--tests/sys/fs/tmpfs/Makefile1
-rw-r--r--tests/sys/geom/Makefile1
-rw-r--r--tests/sys/geom/class/Makefile2
-rw-r--r--tests/sys/geom/class/Makefile.inc1
-rw-r--r--tests/sys/geom/class/concat/Makefile1
-rw-r--r--tests/sys/geom/class/eli/Makefile1
-rw-r--r--tests/sys/geom/class/eli/attach_test.sh29
-rw-r--r--tests/sys/geom/class/gate/Makefile1
-rw-r--r--tests/sys/geom/class/gate/ggate_test.sh12
-rw-r--r--tests/sys/geom/class/mirror/Makefile1
-rw-r--r--tests/sys/geom/class/multipath/Makefile1
-rw-r--r--tests/sys/geom/class/nop/Makefile5
-rw-r--r--tests/sys/geom/class/part/Makefile1
-rw-r--r--tests/sys/geom/class/raid3/Makefile1
-rw-r--r--tests/sys/geom/class/shsec/Makefile1
-rw-r--r--tests/sys/geom/class/stripe/Makefile1
-rw-r--r--tests/sys/geom/class/virstor/Makefile9
-rw-r--r--tests/sys/geom/class/virstor/conf.sh31
-rw-r--r--tests/sys/geom/class/virstor/virstor_test.sh73
-rw-r--r--tests/sys/kern/Makefile18
-rw-r--r--tests/sys/kern/Makefile.inc1
-rw-r--r--tests/sys/kern/acct/Makefile1
-rw-r--r--tests/sys/kern/coredump_phnum_test.sh5
-rw-r--r--tests/sys/kern/execve/Makefile1
-rw-r--r--tests/sys/kern/inotify_test.c862
-rw-r--r--tests/sys/kern/jail_lookup_root.c133
-rw-r--r--tests/sys/kern/jailmeta.sh588
-rw-r--r--tests/sys/kern/kern_copyin.c1
-rw-r--r--tests/sys/kern/ktls_test.c2
-rw-r--r--tests/sys/kern/ktrace_test.c144
-rw-r--r--tests/sys/kern/logsigexit_test.sh37
-rw-r--r--tests/sys/kern/pipe/Makefile1
-rw-r--r--tests/sys/kern/prace.c144
-rw-r--r--tests/sys/kern/ptrace_test.c187
-rw-r--r--tests/sys/kern/sched_affinity.c7
-rw-r--r--tests/sys/kern/sendfile_helper.c2
-rw-r--r--tests/sys/kern/socket_splice.c979
-rw-r--r--tests/sys/kern/sysctl_security_jail_children.sh77
-rw-r--r--tests/sys/kern/tty/Makefile15
-rw-r--r--tests/sys/kern/tty/fionread.c21
-rw-r--r--tests/sys/kern/tty/readsz.c130
-rw-r--r--tests/sys/kern/tty/test_canon.orch102
-rw-r--r--tests/sys/kern/tty/test_canon_fullbuf.orch23
-rw-r--r--tests/sys/kern/tty/test_ncanon.orch39
-rw-r--r--tests/sys/kern/tty/test_recanon.orch90
-rw-r--r--tests/sys/kern/tty/test_sti.c337
-rw-r--r--tests/sys/kern/unix_dgram.c41
-rw-r--r--tests/sys/kern/unix_passfd_test.c180
-rw-r--r--tests/sys/kern/unix_seqpacket_test.c36
-rw-r--r--tests/sys/kern/unix_stream.c401
-rw-r--r--tests/sys/kqueue/Makefile.inc1
-rw-r--r--tests/sys/kqueue/libkqueue/Makefile1
-rw-r--r--tests/sys/kqueue/libkqueue/timer.c2
-rw-r--r--tests/sys/mac/Makefile1
-rw-r--r--tests/sys/mac/Makefile.inc1
-rw-r--r--tests/sys/mac/bsdextended/Makefile1
-rw-r--r--tests/sys/mac/ipacl/Makefile1
-rw-r--r--tests/sys/mac/portacl/Makefile1
-rw-r--r--tests/sys/mqueue/Makefile1
-rw-r--r--tests/sys/net/Makefile3
-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.sh546
-rwxr-xr-xtests/sys/net/if_lagg_test.sh13
-rw-r--r--tests/sys/net/if_ovpn/Makefile4
-rw-r--r--tests/sys/net/if_ovpn/if_ovpn.sh113
-rwxr-xr-xtests/sys/net/if_vlan.sh49
-rw-r--r--tests/sys/net/if_wg.sh299
-rw-r--r--tests/sys/net/routing/Makefile1
-rw-r--r--tests/sys/netgraph/Makefile1
-rw-r--r--tests/sys/netgraph/ksocket.c99
-rw-r--r--tests/sys/netinet/Makefile26
-rwxr-xr-xtests/sys/netinet/arp.sh35
-rw-r--r--tests/sys/netinet/broadcast.c196
-rw-r--r--tests/sys/netinet/carp.py16
-rwxr-xr-xtests/sys/netinet/carp.sh147
-rwxr-xr-xtests/sys/netinet/divert.sh4
-rw-r--r--tests/sys/netinet/fibs_multibind_test.c755
-rw-r--r--tests/sys/netinet/fibs_test.sh45
-rwxr-xr-xtests/sys/netinet/forward.sh61
-rw-r--r--tests/sys/netinet/igmp.py53
-rw-r--r--tests/sys/netinet/ip_reass_test.c12
-rw-r--r--tests/sys/netinet/libalias/2_natout.c170
-rw-r--r--tests/sys/netinet/libalias/Makefile1
-rw-r--r--tests/sys/netinet/libalias/util.c1
-rw-r--r--tests/sys/netinet/libalias/util.h2
-rw-r--r--tests/sys/netinet/multicast.sh61
-rwxr-xr-xtests/sys/netinet/redirect.sh2
-rw-r--r--tests/sys/netinet/sendto-IP_MULTICAST_IF.c63
-rw-r--r--tests/sys/netinet/so_reuseport_lb_test.c322
-rw-r--r--tests/sys/netinet/socket_afinet.c307
-rw-r--r--tests/sys/netinet/tcp_implied_connect.c1
-rw-r--r--tests/sys/netinet/udp_bindings.c249
-rw-r--r--tests/sys/netinet/udp_io.c1
-rw-r--r--tests/sys/netinet6/Makefile56
-rwxr-xr-xtests/sys/netinet6/addr6.sh45
-rwxr-xr-xtests/sys/netinet6/divert.sh3
-rwxr-xr-xtests/sys/netinet6/exthdr.sh2
-rwxr-xr-xtests/sys/netinet6/forward6.sh66
-rw-r--r--tests/sys/netinet6/frag6/Makefile1
-rw-r--r--tests/sys/netinet6/frag6/frag6.subr2
-rwxr-xr-xtests/sys/netinet6/mld.sh2
-rwxr-xr-xtests/sys/netinet6/ndp.sh92
-rw-r--r--tests/sys/netinet6/ra.py38
-rw-r--r--tests/sys/netinet6/redirect.sh8
-rwxr-xr-xtests/sys/netinet6/scapyi386.sh2
-rw-r--r--tests/sys/netipsec/Makefile1
-rw-r--r--tests/sys/netipsec/tunnel/Makefile1
-rwxr-xr-xtests/sys/netipsec/tunnel/empty.sh4
-rw-r--r--tests/sys/netlink/Makefile1
-rw-r--r--tests/sys/netlink/test_rtnl_ifaddr.py20
-rw-r--r--tests/sys/netlink/test_snl.c12
-rw-r--r--tests/sys/netlink/test_snl_generic.c6
-rw-r--r--tests/sys/netmap/Makefile1
-rw-r--r--tests/sys/netmap/ctrl-api-test.c12
-rw-r--r--tests/sys/netpfil/Makefile1
-rw-r--r--tests/sys/netpfil/common/Makefile9
-rw-r--r--tests/sys/netpfil/common/divapp.c (renamed from tests/sys/netpfil/pf/divapp.c)15
-rw-r--r--tests/sys/netpfil/common/dummynet.sh20
-rw-r--r--tests/sys/netpfil/common/forward.sh2
-rw-r--r--tests/sys/netpfil/common/pft_ping.py384
-rw-r--r--tests/sys/netpfil/common/rdr.sh1
-rw-r--r--tests/sys/netpfil/common/sniffer.py13
-rw-r--r--tests/sys/netpfil/common/tos.sh2
-rw-r--r--tests/sys/netpfil/ipfw/Makefile8
-rw-r--r--tests/sys/netpfil/ipfw/divert.sh281
-rw-r--r--tests/sys/netpfil/pf/Makefile39
-rw-r--r--tests/sys/netpfil/pf/altq.sh6
-rw-r--r--tests/sys/netpfil/pf/anchor.sh280
-rw-r--r--tests/sys/netpfil/pf/bsnmpd.conf47
-rw-r--r--tests/sys/netpfil/pf/debug.sh56
-rw-r--r--tests/sys/netpfil/pf/divert-to.sh227
-rw-r--r--tests/sys/netpfil/pf/dup.sh2
-rw-r--r--tests/sys/netpfil/pf/ether.sh18
-rw-r--r--tests/sys/netpfil/pf/forward.sh4
-rw-r--r--tests/sys/netpfil/pf/frag-adjhole.py58
-rw-r--r--tests/sys/netpfil/pf/frag-overhole.py83
-rw-r--r--tests/sys/netpfil/pf/frag6.py248
-rw-r--r--tests/sys/netpfil/pf/fragmentation_compat.sh55
-rw-r--r--tests/sys/netpfil/pf/fragmentation_no_reassembly.sh6
-rw-r--r--tests/sys/netpfil/pf/fragmentation_pass.sh217
-rw-r--r--tests/sys/netpfil/pf/get_state.sh2
-rw-r--r--tests/sys/netpfil/pf/header.py217
-rw-r--r--tests/sys/netpfil/pf/icmp.py254
-rw-r--r--tests/sys/netpfil/pf/icmp.sh69
-rw-r--r--tests/sys/netpfil/pf/icmp6.sh204
-rw-r--r--tests/sys/netpfil/pf/if_enc.sh178
-rw-r--r--tests/sys/netpfil/pf/ioctl/Makefile1
-rw-r--r--tests/sys/netpfil/pf/killstate.sh75
-rw-r--r--tests/sys/netpfil/pf/limits.sh119
-rw-r--r--tests/sys/netpfil/pf/map_e.sh91
-rw-r--r--tests/sys/netpfil/pf/match.sh112
-rw-r--r--tests/sys/netpfil/pf/max_pkt_rate.sh121
-rw-r--r--tests/sys/netpfil/pf/max_pkt_size.sh122
-rwxr-xr-xtests/sys/netpfil/pf/max_states.sh62
-rw-r--r--tests/sys/netpfil/pf/mbuf.sh236
-rw-r--r--tests/sys/netpfil/pf/modulate.sh4
-rw-r--r--tests/sys/netpfil/pf/nat.sh676
-rw-r--r--tests/sys/netpfil/pf/nat64.py274
-rw-r--r--tests/sys/netpfil/pf/nat64.sh1056
-rw-r--r--tests/sys/netpfil/pf/nat66.py17
-rw-r--r--tests/sys/netpfil/pf/pass_block.sh164
-rw-r--r--tests/sys/netpfil/pf/pflog.sh323
-rw-r--r--tests/sys/netpfil/pf/pflow.sh8
-rw-r--r--tests/sys/netpfil/pf/pfsync.sh259
-rw-r--r--tests/sys/netpfil/pf/pft_read_ipfix.py2
-rw-r--r--tests/sys/netpfil/pf/proxy.sh4
-rw-r--r--tests/sys/netpfil/pf/rdr-srcport.py20
-rw-r--r--tests/sys/netpfil/pf/rdr.sh193
-rw-r--r--tests/sys/netpfil/pf/return.py153
-rw-r--r--tests/sys/netpfil/pf/ridentifier.sh16
-rw-r--r--tests/sys/netpfil/pf/route_to.sh80
-rw-r--r--tests/sys/netpfil/pf/rtable.sh4
-rw-r--r--tests/sys/netpfil/pf/rules_counter.sh49
-rw-r--r--tests/sys/netpfil/pf/scrub.sh16
-rw-r--r--tests/sys/netpfil/pf/scrub_compat.sh16
-rw-r--r--tests/sys/netpfil/pf/scrub_pass.sh12
-rw-r--r--tests/sys/netpfil/pf/sctp.py181
-rw-r--r--tests/sys/netpfil/pf/sctp.sh178
-rw-r--r--tests/sys/netpfil/pf/set_skip.sh61
-rw-r--r--tests/sys/netpfil/pf/set_tos.sh20
-rw-r--r--tests/sys/netpfil/pf/snmp.sh123
-rwxr-xr-xtests/sys/netpfil/pf/src_track.sh452
-rw-r--r--tests/sys/netpfil/pf/status.sh73
-rw-r--r--tests/sys/netpfil/pf/syncookie.sh107
-rw-r--r--tests/sys/netpfil/pf/synproxy.sh9
-rw-r--r--tests/sys/netpfil/pf/table.sh266
-rw-r--r--tests/sys/netpfil/pf/tcp.py158
-rw-r--r--tests/sys/netpfil/pf/tcp.sh3
-rw-r--r--tests/sys/netpfil/pf/utils.py46
-rw-r--r--tests/sys/netpfil/pf/utils.subr22
-rw-r--r--tests/sys/opencrypto/Makefile1
-rw-r--r--tests/sys/pjdfstest/Makefile1
-rw-r--r--tests/sys/pjdfstest/Makefile.inc1
-rw-r--r--tests/sys/pjdfstest/pjdfstest/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/Makefile.inc1
-rw-r--r--tests/sys/pjdfstest/tests/chflags/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/chmod/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/chown/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/ftruncate/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/granular/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/link/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/mkdir/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/mkfifo/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/mknod/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/open/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/rename/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/rmdir/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/symlink/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/truncate/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/unlink/Makefile1
-rw-r--r--tests/sys/pjdfstest/tests/utimensat/Makefile1
-rw-r--r--tests/sys/posixshm/Makefile1
-rw-r--r--tests/sys/posixshm/posixshm_test.c29
-rw-r--r--tests/sys/sound/Makefile13
-rw-r--r--tests/sys/sound/pcm_read_write.c262
-rw-r--r--tests/sys/sound/sndstat.c395
-rw-r--r--tests/sys/sys/Makefile3
-rw-r--r--tests/sys/sys/buf_ring_test.c180
-rw-r--r--tests/sys/sys/queue_test.c293
-rw-r--r--tests/sys/vfs/Makefile1
-rw-r--r--tests/sys/vm/Makefile1
-rw-r--r--tests/sys/vm/mmap_test.c70
-rw-r--r--tests/sys/vm/soxstack/Makefile2
-rw-r--r--tests/sys/vm/soxstack/soxstack.c4
-rw-r--r--tests/sys/vm/stack/Makefile1
-rw-r--r--tests/sys/vmm/vmm_cred_jail.sh4
409 files changed, 20205 insertions, 1299 deletions
diff --git a/tests/sys/Makefile b/tests/sys/Makefile
index 20ea4f181c7c..535e627d22cb 100644
--- a/tests/sys/Makefile
+++ b/tests/sys/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
TESTSDIR= ${TESTSBASE}/sys
@@ -7,6 +6,7 @@ TESTS_SUBDIRS+= acl
TESTS_SUBDIRS+= aio
TESTS_SUBDIRS+= ${_audit}
TESTS_SUBDIRS+= auditpipe
+TESTS_SUBDIRS+= cam
TESTS_SUBDIRS+= capsicum
TESTS_SUBDIRS+= ${_cddl}
# XXX: Currently broken in CI
@@ -31,6 +31,7 @@ TESTS_SUBDIRS+= netpfil
TESTS_SUBDIRS+= opencrypto
TESTS_SUBDIRS+= posixshm
TESTS_SUBDIRS+= ses
+TESTS_SUBDIRS+= sound
TESTS_SUBDIRS+= sys
TESTS_SUBDIRS+= vfs
TESTS_SUBDIRS+= vm
diff --git a/tests/sys/Makefile.inc b/tests/sys/Makefile.inc
index 03efce15f856..cec69b26e149 100644
--- a/tests/sys/Makefile.inc
+++ b/tests/sys/Makefile.inc
@@ -1,2 +1 @@
-
.include "${SRCTOP}/tests/Makefile.inc0"
diff --git a/tests/sys/acl/Makefile b/tests/sys/acl/Makefile
index e8db261edaec..0080e8dd5b5a 100644
--- a/tests/sys/acl/Makefile
+++ b/tests/sys/acl/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/acl
diff --git a/tests/sys/aio/Makefile b/tests/sys/aio/Makefile
index b0bddb044268..5cddb28c27a6 100644
--- a/tests/sys/aio/Makefile
+++ b/tests/sys/aio/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/aio
ATF_TESTS_C+= aio_test
diff --git a/tests/sys/aio/aio_kqueue_test.c b/tests/sys/aio/aio_kqueue_test.c
index c2478a9d05b3..5e5cb40d0752 100644
--- a/tests/sys/aio/aio_kqueue_test.c
+++ b/tests/sys/aio/aio_kqueue_test.c
@@ -35,6 +35,7 @@
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
+#include <assert.h>
#include <aio.h>
#include <err.h>
#include <errno.h>
@@ -192,6 +193,7 @@ main (int argc, char *argv[])
for (j = 0; j < max_queue_per_proc && iocb[j] != kq_iocb;
j++) ;
+ assert(j < max_queue_per_proc);
#ifdef DEBUG
printf("kq_iocb %p\n", kq_iocb);
diff --git a/tests/sys/audit/Makefile b/tests/sys/audit/Makefile
index c9068d0a8044..d6d9c2874d09 100644
--- a/tests/sys/audit/Makefile
+++ b/tests/sys/audit/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/audit
ATF_TESTS_C= file-attribute-access
diff --git a/tests/sys/auditpipe/Makefile b/tests/sys/auditpipe/Makefile
index 20832301f044..189535ee74ca 100644
--- a/tests/sys/auditpipe/Makefile
+++ b/tests/sys/auditpipe/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/auditpipe
ATF_TESTS_C= auditpipe_test
diff --git a/tests/sys/cam/Makefile b/tests/sys/cam/Makefile
new file mode 100644
index 000000000000..4cc36604280a
--- /dev/null
+++ b/tests/sys/cam/Makefile
@@ -0,0 +1,7 @@
+.include <src.opts.mk>
+
+TESTSDIR= ${TESTSBASE}/sys/cam
+
+TESTS_SUBDIRS+= ctl
+
+.include <bsd.test.mk>
diff --git a/tests/sys/cam/ctl/Makefile b/tests/sys/cam/ctl/Makefile
new file mode 100644
index 000000000000..05f0831fc8b0
--- /dev/null
+++ b/tests/sys/cam/ctl/Makefile
@@ -0,0 +1,20 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/cam/ctl
+BINDIR=${TESTSDIR}
+
+${PACKAGE}FILES+= ctl.subr
+
+ATF_TESTS_SH+= persist
+ATF_TESTS_SH+= prevent
+ATF_TESTS_SH+= read_buffer
+ATF_TESTS_SH+= start_stop_unit
+
+PROGS+= prout_register_huge_cdb
+LIBADD+= cam
+CFLAGS+= -I${SRCTOP}/sys
+
+# Must be exclusive because it disables/enables camsim
+TEST_METADATA+= is_exclusive="true"
+
+.include <bsd.test.mk>
diff --git a/tests/sys/cam/ctl/ctl.subr b/tests/sys/cam/ctl/ctl.subr
new file mode 100644
index 000000000000..5da441b806f0
--- /dev/null
+++ b/tests/sys/cam/ctl/ctl.subr
@@ -0,0 +1,115 @@
+# vim: filetype=sh
+
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Axcient
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+load_modules() {
+ if ! kldstat -q -m ctl; then
+ kldload ctl || atf_skip "could not load ctl kernel mod"
+ fi
+ if ! ctladm port -o on -p 0; then
+ atf_skip "could not enable the camsim frontend"
+ fi
+}
+
+find_device() {
+ LUN=$1
+
+ # Rescan camsim
+ # XXX camsim doesn't update when creating a new device. Worse, a
+ # rescan won't look for new devices. So we must disable/re-enable it.
+ # Worse still, enabling it isn't synchronous, so we need a retry loop
+ # https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=281000
+ retries=5
+ ctladm port -o off -p 0 >/dev/null
+ ctladm port -o on -p 0 >/dev/null
+ HEXLUN=`printf %x $LUN`
+ while true; do
+ dev=`camcontrol devlist | awk -v lun=$HEXLUN '
+ /FREEBSD CTL.*,pass/ && $9==lun {
+ split($10, fields, /[,]/); print fields[1];
+ }
+ /FREEBSD CTL.*\(pass/ && $9==lun {
+ split($10, fields, /[,]/); print fields[2];
+ }
+ ' | sed 's:[()]::'`
+ if [ -z "$dev" -o ! -c /dev/$dev ]; then
+ retries=$(( $retries - 1 ))
+ if [ $retries -eq 0 ]; then
+ cat lun-create.txt
+ camcontrol devlist
+ atf_fail "Could not find GEOM device"
+ fi
+ sleep 0.1
+ continue
+ fi
+ break
+ done
+ # Ensure that it's actually ready. camcontrol may report the disk's
+ # ident before it's actually ready to receive commands. Maybe that's
+ # because all of the GEOM providers must probe it?
+ while true; do
+ dd if=/dev/$dev bs=4096 count=1 of=/dev/null >/dev/null 2>/dev/null && break
+ retries=$(( $retries - 1 ))
+ if [ $retries -eq 0 ]; then
+ atf_fail "Device never became ready"
+ fi
+ sleep 0.1
+ done
+}
+
+# Create a CTL LUN backed by a file
+create_block() {
+ EXTRA_ARGS=$*
+
+ atf_check -o save:lun-create.txt ctladm create -b block $EXTRA_ARGS
+ atf_check egrep -q "LUN created successfully" lun-create.txt
+ LUN=`awk '/LUN ID:/ {print $NF}' lun-create.txt`
+ if [ -z "$LUN" ]; then
+ atf_fail "Could not find LUN id"
+ fi
+ find_device $LUN
+}
+
+# Create a CTL LUN backed by RAM
+create_ramdisk() {
+ EXTRA_ARGS=$*
+
+ atf_check -o save:lun-create.txt ctladm create -b ramdisk -s 1048576 $EXTRA_ARGS
+ atf_check egrep -q "LUN created successfully" lun-create.txt
+ LUN=`awk '/LUN ID:/ {print $NF}' lun-create.txt`
+ if [ -z "$LUN" ]; then
+ atf_fail "Could not find LUN id"
+ fi
+ find_device $LUN
+}
+
+cleanup() {
+ if [ -e "lun-create.txt" ]; then
+ backend=`awk '/backend:/ {print $NF}' lun-create.txt`
+ lun_id=`awk '/LUN ID:/ {print $NF}' lun-create.txt`
+ ctladm remove -b $backend -l $lun_id > /dev/null
+ fi
+}
diff --git a/tests/sys/cam/ctl/persist.sh b/tests/sys/cam/ctl/persist.sh
new file mode 100644
index 000000000000..2a350ee4775a
--- /dev/null
+++ b/tests/sys/cam/ctl/persist.sh
@@ -0,0 +1,349 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 ConnectWise
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+. $(atf_get_srcdir)/ctl.subr
+
+# TODO
+# * PRIN READ RESERVATION, with one reservation
+# * PROUT with illegal type
+# * PROUT REGISTER AND IGNORE EXISTING KEY
+# * PROUT REGISTER AND IGNORE EXISTING KEY with a RESERVATION KEY that isn't registered
+# * PROUT REGISTER AND IGNORE EXISTING KEY to unregister
+# * PROUT CLEAR allows previously prevented medium removal
+# * PROUT PREEMPT
+# * PROUT PREEMPT with a RESERVATION KEY that isn't registered
+# * PROUT PREEMPT_AND_ABORT
+# * PROUT PREEMPT_AND_ABORT with a RESERVATION KEY that isn't registered
+# * PROUT REGISTER AND MOVE
+# * PROUT REGISTER AND MOVE with a RESERVATION KEY that isn't registered
+# * multiple initiators
+
+# Not Tested
+# * PROUT REPLACE LOST RESERVATION (not supported by ctl)
+# * Specify Initiator Ports bit (not supported by ctl)
+# * Activate Persist Through Power Loss bit (not supported by ctl)
+# * All Target Ports bit (not supported by ctl)
+
+RESERVATION_KEY=0xdeadbeef1a7ebabe
+
+atf_test_case prin_read_full_status_empty cleanup
+prin_read_full_status_empty_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION IN with the READ FULL STATUS service action, with no status descriptors"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prin_read_full_status_empty_body()
+{
+ create_ramdisk
+
+ atf_check -o match:"No full status descriptors" sg_persist -ns /dev/$dev
+}
+prin_read_full_status_empty_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prin_read_keys_empty cleanup
+prin_read_keys_empty_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION IN with the READ KEYS service action, with no registered keys"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prin_read_keys_empty_body()
+{
+ create_ramdisk
+
+ atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev
+}
+prin_read_keys_empty_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prin_read_reservation_empty cleanup
+prin_read_reservation_empty_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION IN with the READ RESERVATION service action, with no reservations"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prin_read_reservation_empty_body()
+{
+ create_ramdisk
+
+ atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev
+}
+prin_read_reservation_empty_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prin_report_capabilities cleanup
+prin_report_capabilities_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION IN with the REPORT CAPABILITIES service action"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prin_report_capabilities_body()
+{
+ create_ramdisk
+
+ cat > expected <<HERE
+Report capabilities response:
+ Replace Lost Reservation Capable(RLR_C): 0
+ Compatible Reservation Handling(CRH): 1
+ Specify Initiator Ports Capable(SIP_C): 0
+ All Target Ports Capable(ATP_C): 0
+ Persist Through Power Loss Capable(PTPL_C): 0
+ Type Mask Valid(TMV): 1
+ Allow Commands: 5
+ Persist Through Power Loss Active(PTPL_A): 0
+ Support indicated in Type mask:
+ Write Exclusive, all registrants: 1
+ Exclusive Access, registrants only: 1
+ Write Exclusive, registrants only: 1
+ Exclusive Access: 1
+ Write Exclusive: 1
+ Exclusive Access, all registrants: 1
+HERE
+ atf_check -o file:expected sg_persist -nc /dev/$dev
+}
+prin_report_capabilities_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_clear cleanup
+prout_clear_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT with the CLEAR service action"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_clear_body()
+{
+ create_ramdisk
+
+ # First register a key
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+
+ # Then make a reservation using that key
+ atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev
+
+ # Now, clear all reservations and registrations
+ atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --clear /dev/$dev
+
+ # Finally, check that all reservations and keys are gone
+ atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev
+ atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev
+}
+prout_clear_cleanup()
+{
+ cleanup
+}
+
+
+atf_test_case prout_register cleanup
+prout_register_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT with the REGISTER service action"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_register_body()
+{
+ create_ramdisk
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+ atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev
+}
+prout_register_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_register_duplicate cleanup
+prout_register_duplicate_head()
+{
+ atf_set "descr" "attempting to register a key twice should fail"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_register_duplicate_body()
+{
+ create_ramdisk
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+ atf_check -s exit:24 -e match:"Reservation conflict" sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+ atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev
+}
+prout_register_duplicate_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_register_huge_cdb cleanup
+prout_register_huge_cdb_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT with an enormous CDB size should not cause trouble"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_register_huge_cdb_body()
+{
+ create_ramdisk
+
+ atf_check -s exit:1 $(atf_get_srcdir)/prout_register_huge_cdb $LUN
+}
+prout_register_huge_cdb_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_register_unregister cleanup
+prout_register_unregister_head()
+{
+ atf_set "descr" "use PERSISTENT RESERVATION OUT with the REGISTER service action to remove a prior registration"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_register_unregister_body()
+{
+ create_ramdisk
+ # First register a key
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+ # Then unregister it
+ atf_check sg_persist -n --out --param-sark=0 --param-rk=$RESERVATION_KEY -G /dev/$dev
+ # Finally, check that no keys are registered
+ atf_check -o match:"there are NO registered reservation keys" sg_persist -nk /dev/$dev
+}
+prout_register_unregister_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_release cleanup
+prout_release_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT with the RESERVE service action"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_release_body()
+{
+ create_ramdisk
+
+ # First register a key
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+
+ # Then make a reservation using that key
+ atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev
+ atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --prout-type=8 --release /dev/$dev
+
+ # Now check that the reservation is released
+ atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev
+ # But the registration shouldn't be.
+ atf_check -o match:$RESERVATION_KEY sg_persist -nk /dev/$dev
+}
+prout_release_cleanup()
+{
+ cleanup
+}
+
+
+atf_test_case prout_reserve cleanup
+prout_reserve_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT with the RESERVE service action"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist ctladm
+}
+prout_reserve_body()
+{
+ create_ramdisk
+ # First register a key
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+ # Then make a reservation using that key
+ atf_check sg_persist -n --out --param-rk=$RESERVATION_KEY --reserve --prout-type=8 /dev/$dev
+ # Finally, check that the reservation is correct
+ cat > expected <<HERE
+ PR generation=0x1
+ Key=0xdeadbeef1a7ebabe
+ All target ports bit clear
+ Relative port address: 0x0
+ << Reservation holder >>
+ scope: LU_SCOPE, type: Exclusive Access, all registrants
+ Transport Id of initiator:
+ Parallel SCSI initiator SCSI address: 0x1
+ relative port number (of corresponding target): 0x0
+HERE
+ atf_check -o file:expected sg_persist -ns /dev/$dev
+}
+prout_reserve_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prout_reserve_bad_scope cleanup
+prout_reserve_bad_scope_head()
+{
+ atf_set "descr" "PERSISTENT RESERVATION OUT will be rejected with an unknown scope field"
+ atf_set "require.user" "root"
+ atf_set "require.progs" sg_persist camcontrol ctladm
+}
+prout_reserve_bad_scope_body()
+{
+ create_ramdisk
+ # First register a key
+ atf_check sg_persist -n --out --param-rk=0 --param-sark=$RESERVATION_KEY -G /dev/$dev
+
+ # Then make a reservation using that key
+ atf_check -s exit:1 -e match:"ILLEGAL REQUEST asc:24,0 .Invalid field in CDB." camcontrol persist $dev -o reserve -k $RESERVATION_KEY -T read_shared -s 15 -v
+
+ # Finally, check that nothing has been reserved
+ atf_check -o match:"there is NO reservation held" sg_persist -nr /dev/$dev
+}
+prout_reserve_bad_scope_cleanup()
+{
+ cleanup
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case prin_read_full_status_empty
+ atf_add_test_case prin_read_keys_empty
+ atf_add_test_case prin_read_reservation_empty
+ atf_add_test_case prin_report_capabilities
+ atf_add_test_case prout_clear
+ atf_add_test_case prout_register
+ atf_add_test_case prout_register_duplicate
+ atf_add_test_case prout_register_huge_cdb
+ atf_add_test_case prout_register_unregister
+ atf_add_test_case prout_release
+ atf_add_test_case prout_reserve
+ atf_add_test_case prout_reserve_bad_scope
+}
diff --git a/tests/sys/cam/ctl/prevent.sh b/tests/sys/cam/ctl/prevent.sh
new file mode 100644
index 000000000000..315bedc39581
--- /dev/null
+++ b/tests/sys/cam/ctl/prevent.sh
@@ -0,0 +1,161 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Axcient
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+. $(atf_get_srcdir)/ctl.subr
+
+# TODO
+# * multiple initiators may block removal
+
+# Not Tested
+# * persistent removal (not implemented in CTL)
+
+atf_test_case allow cleanup
+allow_head()
+{
+ atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL will prevent a CD from being ejected"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_prevent sg_start ctladm"
+}
+allow_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ atf_check sg_prevent --prevent 1 /dev/$dev
+
+ # Now sg_start --eject should fail
+ atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev
+
+ atf_check sg_prevent --allow /dev/$dev
+
+ # Now sg_start --eject should work again
+ atf_check -s exit:0 sg_start --eject /dev/$dev
+}
+allow_cleanup()
+{
+ cleanup
+}
+
+atf_test_case allow_idempotent cleanup
+allow_idempotent_head()
+{
+ atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL is idempotent when run from the same initiator"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_prevent sg_start ctladm"
+}
+allow_idempotent_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ atf_check sg_prevent --allow /dev/$dev
+ atf_check sg_prevent --allow /dev/$dev
+ atf_check sg_prevent --prevent 1 /dev/$dev
+
+ # Even though we ran --allow twice, a single --prevent command should
+ # suffice to prevent ejecting. Multiple ALLOW/PREVENT commands from
+ # the same initiator don't have any additional effect.
+ atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev
+}
+allow_idempotent_cleanup()
+{
+ cleanup
+}
+
+atf_test_case nonremovable cleanup
+nonremovable_head()
+{
+ atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL may not be used on non-removable media"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_prevent ctladm"
+}
+nonremovable_body()
+{
+ # Create a HDD, not a CD, device
+ create_ramdisk -t 0
+
+ atf_check -s exit:9 -e match:"Invalid opcode" sg_prevent /dev/$dev
+}
+nonremovable_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prevent cleanup
+prevent_head()
+{
+ atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL will prevent a CD from being ejected"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_prevent sg_start ctladm"
+}
+prevent_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ atf_check sg_prevent --prevent 1 /dev/$dev
+
+ # Now sg_start --eject should fail
+ atf_check -s exit:5 -e match:"Illegal request" sg_start --eject /dev/$dev
+}
+prevent_cleanup()
+{
+ cleanup
+}
+
+atf_test_case prevent_idempotent cleanup
+prevent_idempotent_head()
+{
+ atf_set "descr" "SCSI PREVENT ALLOW MEDIUM REMOVAL is idempotent when run from the same initiator"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_prevent sg_start ctladm"
+}
+prevent_idempotent_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ atf_check sg_prevent --prevent 1 /dev/$dev
+ atf_check sg_prevent --prevent 1 /dev/$dev
+ atf_check sg_prevent --allow /dev/$dev
+
+ # Even though we ran prevent idempotent and allow only once, eject
+ # should be allowed. Multiple PREVENT commands from the same initiator
+ # don't have any additional effect.
+ atf_check sg_start --eject /dev/$dev
+}
+prevent_idempotent_cleanup()
+{
+ cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case allow
+ atf_add_test_case allow_idempotent
+ atf_add_test_case nonremovable
+ atf_add_test_case prevent
+ atf_add_test_case prevent_idempotent
+}
diff --git a/tests/sys/cam/ctl/prout_register_huge_cdb.c b/tests/sys/cam/ctl/prout_register_huge_cdb.c
new file mode 100644
index 000000000000..f57a6abfadd6
--- /dev/null
+++ b/tests/sys/cam/ctl/prout_register_huge_cdb.c
@@ -0,0 +1,88 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 ConnectWise
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * Helper that sends a PERSISTENT RESERVATION OUT command to CTL with a
+ * ridiculously huge size for the length of the CDB. This is not possible with
+ * ctladm, for good reason.
+ */
+#include <camlib.h>
+#include <err.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+
+#include <cam/scsi/scsi_message.h>
+#include <cam/ctl/ctl_io.h>
+#include <cam/ctl/ctl.h>
+#include <cam/ctl/ctl_ioctl.h>
+#include <cam/ctl/ctl_util.h>
+
+int
+main(int argc, char **argv)
+{
+ union ctl_io *io;
+ int fd = open("/dev/cam/ctl", O_RDWR);
+ int r;
+ uint32_t targ_port;
+
+ if (argc < 2)
+ errx(2, "usage: prout_register_huge_cdb <target_port>\n");
+
+ targ_port = strtoul(argv[1], NULL, 10);
+
+ io = calloc(1, sizeof(*io));
+ io->io_hdr.nexus.initid = 7; /* 7 is ctladm's default initiator id */
+ io->io_hdr.nexus.targ_port = targ_port;
+ io->io_hdr.nexus.targ_mapped_lun = 0;
+ io->io_hdr.nexus.targ_lun = 0;
+ io->io_hdr.io_type = CTL_IO_SCSI;
+ io->taskio.tag_type = CTL_TAG_UNTAGGED;
+ uint8_t cdb[32] = {};
+ // ctl_persistent_reserve_out// 5f 00
+ cdb[0] = 0x5f;
+ cdb[1] = 0x00;
+ struct scsi_per_res_out *cdb_ = ( struct scsi_per_res_out *)cdb;
+ // Claim an enormous size of the CDB, but don't actually alloc it all.
+ cdb_->length[0] = 0xff;
+ cdb_->length[1] = 0xff;
+ cdb_->length[2] = 0xff;
+ cdb_->length[3] = 0xff;
+ io->scsiio.cdb_len = sizeof(cdb);
+ memcpy(io->scsiio.cdb, cdb, sizeof(cdb));
+ io->io_hdr.flags |= CTL_FLAG_DATA_IN;
+ r = ioctl(fd, CTL_IO, io);
+ if (r == -1)
+ err(1, "ioctl");
+ if ((io->io_hdr.status & CTL_STATUS_MASK) == CTL_SUCCESS) {
+ return (0);
+ } else {
+ return (1);
+ }
+}
diff --git a/tests/sys/cam/ctl/read_buffer.sh b/tests/sys/cam/ctl/read_buffer.sh
new file mode 100644
index 000000000000..98515943dd40
--- /dev/null
+++ b/tests/sys/cam/ctl/read_buffer.sh
@@ -0,0 +1,172 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Axcient
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# Not tested
+# * modes other than "Data" and "Desc". We don't support those.
+# * Buffer ID other than 0. We don't support those.
+# * The Mode Specific field. We don't support it.
+
+. $(atf_get_srcdir)/ctl.subr
+
+atf_test_case basic cleanup
+basic_head()
+{
+ atf_set "descr" "READ BUFFER can retrieve data previously written by WRITE BUFFER"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm"
+}
+basic_body()
+{
+ create_ramdisk
+
+ # Write to its buffer
+ cp /etc/passwd input
+ len=`wc -c input | cut -wf 2`
+ atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev
+
+ # Read it back
+ atf_check -o save:output sg_read_buffer --mode data -l $len --raw /dev/$dev
+
+ # And verify
+ if ! diff -q input output; then
+ atf_fail "Miscompare!"
+ fi
+}
+basic_cleanup()
+{
+ cleanup
+}
+
+# Read from the Descriptor mode. Along with Data, these are the only two modes
+# we support.
+atf_test_case desc cleanup
+desc_head()
+{
+ atf_set "descr" "READ BUFFER can retrieve the buffer size via the DESCRIPTOR mode"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_read_buffer ctladm"
+}
+desc_body()
+{
+ create_ramdisk
+
+ atf_check -o inline:" 00 00 04 00 00\n" sg_read_buffer --hex --mode desc /dev/$dev
+}
+desc_cleanup()
+{
+ cleanup
+}
+
+atf_test_case length cleanup
+length_head()
+{
+ atf_set "descr" "READ BUFFER can limit its length with the LENGTH field"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm"
+}
+length_body()
+{
+ create_ramdisk
+
+ # Write to its buffer
+ atf_check -o ignore -e ignore dd if=/dev/random of=input bs=4096 count=1
+ atf_check -o ignore -e ignore dd if=input bs=2048 count=1 of=expected
+ atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev
+
+ # Read it back
+ atf_check -o save:output sg_read_buffer --mode data -l 2048 --raw /dev/$dev
+
+ # And verify
+ if ! diff -q expected output; then
+ atf_fail "Miscompare!"
+ fi
+}
+length_cleanup()
+{
+ cleanup
+}
+
+atf_test_case offset cleanup
+offset_head()
+{
+ atf_set "descr" "READ BUFFER accepts the BUFFER OFFSET field"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_read_buffer sg_write_buffer ctladm"
+}
+offset_body()
+{
+ create_ramdisk
+
+ # Write to its buffer
+ atf_check -o ignore -e ignore dd if=/dev/random of=input bs=4096 count=1
+ atf_check -o ignore -e ignore dd if=input iseek=2 bs=512 count=1 of=expected
+ atf_check -o ignore sg_write_buffer --mode data --in=input /dev/$dev
+
+ # Read it back
+ atf_check -o save:output sg_read_buffer --mode data -l 512 -o 1024 --raw /dev/$dev
+
+ # And verify
+ if ! diff -q expected output; then
+ atf_fail "Miscompare!"
+ fi
+}
+offset_cleanup()
+{
+ cleanup
+}
+
+atf_test_case uninitialized cleanup
+uninitialized_head()
+{
+ atf_set "descr" "READ BUFFER buffers are zero-initialized"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_read_buffer ctladm"
+}
+uninitialized_body()
+{
+ create_ramdisk
+
+ # Read an uninitialized buffer
+ atf_check -o save:output sg_read_buffer --mode data -l 262144 --raw /dev/$dev
+
+ # And verify
+ atf_check -o ignore -e ignore dd if=/dev/zero bs=262144 count=1 of=expected
+ if ! diff -q expected output; then
+ atf_fail "Miscompare!"
+ fi
+}
+uninitialized_cleanup()
+{
+ cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case basic
+ atf_add_test_case desc
+ atf_add_test_case length
+ atf_add_test_case offset
+ atf_add_test_case uninitialized
+}
diff --git a/tests/sys/cam/ctl/start_stop_unit.sh b/tests/sys/cam/ctl/start_stop_unit.sh
new file mode 100644
index 000000000000..a1160b35e4a7
--- /dev/null
+++ b/tests/sys/cam/ctl/start_stop_unit.sh
@@ -0,0 +1,150 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Axcient
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+. $(atf_get_srcdir)/ctl.subr
+
+# TODO:
+# * format layer
+# * IMM bit
+# * LOEJ
+# * noflush
+# * power conditions
+
+# Not Tested
+# * Power Condition Modifier (not implemented in CTL)
+
+atf_test_case eject cleanup
+eject_head()
+{
+ atf_set "descr" "START STOP UNIT can eject a CDROM device"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_start sg_readcap ctladm"
+}
+eject_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ # Verify that the device is online
+ # Too bad I don't know of any other way to check that it's stopped but
+ # by using sg_readcap.
+ atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev
+
+ # eject the device
+ atf_check sg_start --eject /dev/$dev
+
+ # Ejected, it should now return ENXIO
+ atf_check -s exit:1 -o ignore -e match:"Device not configured" dd if=/dev/$dev bs=4096 count=1 of=/dev/null
+}
+eject_cleanup()
+{
+ cleanup
+}
+
+atf_test_case load cleanup
+load_head()
+{
+ atf_set "descr" "START STOP UNIT can load a CDROM device"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_start sg_readcap ctladm"
+}
+load_body()
+{
+ # -t 5 for CD/DVD device type
+ create_ramdisk -t 5
+
+ # eject the device
+ atf_check sg_start --eject /dev/$dev
+
+ # Verify that it's offline it should now return ENXIO
+ atf_check -s exit:1 -o ignore -e match:"Device not configured" dd if=/dev/$dev bs=4096 count=1 of=/dev/null
+
+ # Load it again
+ atf_check sg_start --load /dev/$dev
+
+ atf_check -o ignore -e ignore dd if=/dev/$dev bs=4096 count=1 of=/dev/null
+ atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev
+}
+load_cleanup()
+{
+ cleanup
+}
+
+atf_test_case start cleanup
+start_head()
+{
+ atf_set "descr" "START STOP UNIT can start a device"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_start sg_readcap ctladm"
+}
+start_body()
+{
+ create_ramdisk
+
+ # stop the device
+ atf_check sg_start --stop /dev/$dev
+
+ # And start it again
+ atf_check sg_start /dev/$dev
+
+ # Now sg_readcap should succeed. Too bad I don't know of any other way
+ # to check that it's stopped.
+ atf_check -o ignore -e not-match:"Device not ready" sg_readcap /dev/$dev
+}
+start_cleanup()
+{
+ cleanup
+}
+
+atf_test_case stop cleanup
+stop_head()
+{
+ atf_set "descr" "START STOP UNIT can stop a device"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "sg_start sg_readcap ctladm"
+}
+stop_body()
+{
+ create_ramdisk
+
+ # Stop the device
+ atf_check sg_start --stop /dev/$dev
+
+ # Now sg_readcap should fail. Too bad I don't know of any other way to
+ # check that it's stopped.
+ atf_check -s exit:2 -e match:"Device not ready" sg_readcap /dev/$dev
+}
+stop_cleanup()
+{
+ cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case eject
+ atf_add_test_case load
+ atf_add_test_case start
+ atf_add_test_case stop
+}
diff --git a/tests/sys/capsicum/Makefile b/tests/sys/capsicum/Makefile
index 81cb4fa1ceee..fd8dcb29d65c 100644
--- a/tests/sys/capsicum/Makefile
+++ b/tests/sys/capsicum/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
TESTSDIR= ${TESTSBASE}/sys/capsicum
@@ -14,6 +13,10 @@ CFLAGS+= -I${SRCTOP}/tests
GTESTS+= capsicum-test
GTESTS_WRAPPER_SH.capsicum-test= functional
+# This test script runs the same test suite twice, once as root and once as an
+# unprivileged user. Serialize them since some tests access global namespaces,
+# e.g., mqueuefs, and can trample on each other.
+TEST_METADATA.functional+= is_exclusive="true"
SRCS.capsicum-test+= \
capsicum-test-main.cc \
diff --git a/tests/sys/cddl/Makefile b/tests/sys/cddl/Makefile
index 80c72ea5ec42..66377e1e3bfd 100644
--- a/tests/sys/cddl/Makefile
+++ b/tests/sys/cddl/Makefile
@@ -1,11 +1,10 @@
-
.include <src.opts.mk>
TESTSDIR= ${TESTSBASE}/sys/cddl
TESTS_SUBDIRS+= ${_zfs}
-.if ${MK_ZFS} != "no"
+.if ${MK_ZFS_TESTS} != "no"
_zfs= zfs
.endif
diff --git a/tests/sys/cddl/zfs/Makefile b/tests/sys/cddl/zfs/Makefile
index 18097f9b2617..f215f7438e78 100644
--- a/tests/sys/cddl/zfs/Makefile
+++ b/tests/sys/cddl/zfs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
TESTSDIR= ${TESTSBASE}/sys/cddl/zfs
diff --git a/tests/sys/cddl/zfs/bin/Makefile b/tests/sys/cddl/zfs/bin/Makefile
index 3dfb86b1bd5b..98db25e396d3 100644
--- a/tests/sys/cddl/zfs/bin/Makefile
+++ b/tests/sys/cddl/zfs/bin/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/include/Makefile b/tests/sys/cddl/zfs/include/Makefile
index 81e81cb2e0c7..c7712a7d0a88 100644
--- a/tests/sys/cddl/zfs/include/Makefile
+++ b/tests/sys/cddl/zfs/include/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/tests/Makefile b/tests/sys/cddl/zfs/tests/Makefile
index efaae7bfc2c5..f007e78992fc 100644
--- a/tests/sys/cddl/zfs/tests/Makefile
+++ b/tests/sys/cddl/zfs/tests/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/tests/Makefile.inc b/tests/sys/cddl/zfs/tests/Makefile.inc
index 069272ab8104..1b911c451c01 100644
--- a/tests/sys/cddl/zfs/tests/Makefile.inc
+++ b/tests/sys/cddl/zfs/tests/Makefile.inc
@@ -1,2 +1 @@
-
WARNS?= 0
diff --git a/tests/sys/cddl/zfs/tests/acl/Makefile b/tests/sys/cddl/zfs/tests/acl/Makefile
index 062a007a4096..95cbc40972e5 100644
--- a/tests/sys/cddl/zfs/tests/acl/Makefile
+++ b/tests/sys/cddl/zfs/tests/acl/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/acl/cifs/Makefile b/tests/sys/cddl/zfs/tests/acl/cifs/Makefile
index 30bfd2183a4d..bbb54429ec2c 100644
--- a/tests/sys/cddl/zfs/tests/acl/cifs/Makefile
+++ b/tests/sys/cddl/zfs/tests/acl/cifs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile b/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile
index f942fa2ace46..1332371f7570 100644
--- a/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile
+++ b/tests/sys/cddl/zfs/tests/acl/nontrivial/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/acl/trivial/Makefile b/tests/sys/cddl/zfs/tests/acl/trivial/Makefile
index ca54995a53a0..5a00bc05746b 100644
--- a/tests/sys/cddl/zfs/tests/acl/trivial/Makefile
+++ b/tests/sys/cddl/zfs/tests/acl/trivial/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/atime/Makefile b/tests/sys/cddl/zfs/tests/atime/Makefile
index 315123c59bbd..f89f32c149f3 100644
--- a/tests/sys/cddl/zfs/tests/atime/Makefile
+++ b/tests/sys/cddl/zfs/tests/atime/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/bootfs/Makefile b/tests/sys/cddl/zfs/tests/bootfs/Makefile
index f59b08261989..53e2224e1d8c 100644
--- a/tests/sys/cddl/zfs/tests/bootfs/Makefile
+++ b/tests/sys/cddl/zfs/tests/bootfs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cache/Makefile b/tests/sys/cddl/zfs/tests/cache/Makefile
index 1955009834fe..df46dedcc8ec 100644
--- a/tests/sys/cddl/zfs/tests/cache/Makefile
+++ b/tests/sys/cddl/zfs/tests/cache/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cachefile/Makefile b/tests/sys/cddl/zfs/tests/cachefile/Makefile
index 60bfda066a15..09b55f1681aa 100644
--- a/tests/sys/cddl/zfs/tests/cachefile/Makefile
+++ b/tests/sys/cddl/zfs/tests/cachefile/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/clean_mirror/Makefile b/tests/sys/cddl/zfs/tests/clean_mirror/Makefile
index a95d1a286df5..881d6cdedb7a 100644
--- a/tests/sys/cddl/zfs/tests/clean_mirror/Makefile
+++ b/tests/sys/cddl/zfs/tests/clean_mirror/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/Makefile b/tests/sys/cddl/zfs/tests/cli_root/Makefile
index 33b97022fc4f..05b2f480e006 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile
index 33ae1fcdbdc4..7bf850245a84 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zdb/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile
index 814b790f1585..e123a67e482b 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile
index ea8808442e1e..017e2c312ac6 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_clone/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile
index 6bb2031caf6b..f608eca9abf2 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_copies/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile
index 0a46a9e0b4c3..6f048fd37e7a 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_create/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile
index 0aff64173c45..8e8777ff42af 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_destroy/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile
index d2e61fdc8aae..2b6288c76ba8 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_diff/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile
index 2763a53db4ee..140e5634d23f 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_get/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile
index 0949c0408257..3b532596cee1 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_inherit/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile
index 4d06e3e082a8..b5b68263d8a5 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_mount/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile
index 94929d25dcf4..53c59cf0bd99 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_promote/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile
index ae621146fef3..91b14e1aeadd 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_property/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile
index b71daeb93518..066039a3fa04 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_receive/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile
index 6794da9b054f..431ab61354b4 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_rename/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile
index eb1b17251ae6..07f9e88fe963 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_reservation/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile
index f8426d398d79..d288ea35914b 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_rollback/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile
index 6b505a3bee2a..ff100e37f330 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_send/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile
index 10e68def5db2..69300727fd9e 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_set/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile
index 29b992e97c2c..1027523ddcd6 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_share/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile
index f1a7f4a4e7fa..7cad43c9d796 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_snapshot/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile
index 930191f2b7bb..ba37c8fc0f62 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_unmount/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile
index e5d0ac041940..eeaceb593ef7 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_unshare/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile
index a26461b62104..0b01b8f05aba 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zfs_upgrade/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile
index fbbdb8c3dcf5..1d09a43ee977 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile
index ed89d7ca995f..27986f8207fd 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_add/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile
index 24ad0b17f737..d094cf65cbfd 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_attach/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile
index a40272300789..de2992fe86b3 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_clear/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile
index a331da6db05f..73026cf21ada 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_create/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile
index a1be860244a7..90ceee214b00 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_destroy/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile
index 7e4d63ea4c0c..353b8d22be79 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_detach/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile
index 0d1f98cab009..68265a18bbd0 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_expand/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile
index 5e87a94da1b7..fb15c0421a31 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_export/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile
index b24ce38a3567..7066d97f02bc 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_get/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile
index f2e930ad2fc1..c1341eaeec67 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_history/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile
index a946dd6ceeba..508bfef28e4c 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile
index 4f7e6e1a1054..13235edd1f47 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_import/blockfiles/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile
index e1b55de436d8..22728ca40310 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_offline/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile
index 28ee77f2de35..73d3cb438b87 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_online/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile
index 1de45591b801..39fdf272482f 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_remove/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile
index 00302f3833ef..dfe0a7c265d5 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_replace/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile
index fed1bd5a1094..5c855230c0d7 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_scrub/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile
index 0477b0d99a64..4e8936cdb3b1 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_set/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile
index 74b7a66e5e72..4340af728892 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_status/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile
index 583d490387ab..f6365bbb46d5 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile
index a31ad913b50c..2380e3be9e43 100644
--- a/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_root/zpool_upgrade/blockfiles/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_user/Makefile b/tests/sys/cddl/zfs/tests/cli_user/Makefile
index 667d4133ae77..947337d58367 100644
--- a/tests/sys/cddl/zfs/tests/cli_user/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_user/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile b/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile
index 85ff511360a9..d22caa7382c5 100644
--- a/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_user/misc/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile
index 5443ff4dec0d..14185efb74da 100644
--- a/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_user/zfs_list/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile
index 4de5d3857568..4c66f6d8343a 100644
--- a/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_user/zpool_iostat/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile b/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile
index d9f524c75d85..b50c82ccc1b4 100644
--- a/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile
+++ b/tests/sys/cddl/zfs/tests/cli_user/zpool_list/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/compression/Makefile b/tests/sys/cddl/zfs/tests/compression/Makefile
index 3ad018047e6e..3ed64fc3460e 100644
--- a/tests/sys/cddl/zfs/tests/compression/Makefile
+++ b/tests/sys/cddl/zfs/tests/compression/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/ctime/Makefile b/tests/sys/cddl/zfs/tests/ctime/Makefile
index 752ff9695a49..d7cf76d4517c 100644
--- a/tests/sys/cddl/zfs/tests/ctime/Makefile
+++ b/tests/sys/cddl/zfs/tests/ctime/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/delegate/Makefile b/tests/sys/cddl/zfs/tests/delegate/Makefile
index 5fb0be849309..c25841283c20 100644
--- a/tests/sys/cddl/zfs/tests/delegate/Makefile
+++ b/tests/sys/cddl/zfs/tests/delegate/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/devices/Makefile b/tests/sys/cddl/zfs/tests/devices/Makefile
index 4734f61096cf..6902f8c270be 100644
--- a/tests/sys/cddl/zfs/tests/devices/Makefile
+++ b/tests/sys/cddl/zfs/tests/devices/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/exec/Makefile b/tests/sys/cddl/zfs/tests/exec/Makefile
index 33dfcfad177b..2a8efb84b079 100644
--- a/tests/sys/cddl/zfs/tests/exec/Makefile
+++ b/tests/sys/cddl/zfs/tests/exec/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/grow_pool/Makefile b/tests/sys/cddl/zfs/tests/grow_pool/Makefile
index 9537fb62fde4..31fdf1fc9b39 100644
--- a/tests/sys/cddl/zfs/tests/grow_pool/Makefile
+++ b/tests/sys/cddl/zfs/tests/grow_pool/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/grow_replicas/Makefile b/tests/sys/cddl/zfs/tests/grow_replicas/Makefile
index 1eebf5ef0a31..06ee0c68c435 100644
--- a/tests/sys/cddl/zfs/tests/grow_replicas/Makefile
+++ b/tests/sys/cddl/zfs/tests/grow_replicas/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/history/Makefile b/tests/sys/cddl/zfs/tests/history/Makefile
index 8cd1babd3ecd..8a8175abbaea 100644
--- a/tests/sys/cddl/zfs/tests/history/Makefile
+++ b/tests/sys/cddl/zfs/tests/history/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/hotplug/Makefile b/tests/sys/cddl/zfs/tests/hotplug/Makefile
index 3da1b0f4174b..2264668f14bd 100644
--- a/tests/sys/cddl/zfs/tests/hotplug/Makefile
+++ b/tests/sys/cddl/zfs/tests/hotplug/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/hotspare/Makefile b/tests/sys/cddl/zfs/tests/hotspare/Makefile
index 9371186866b0..02a7aa3a1ac4 100644
--- a/tests/sys/cddl/zfs/tests/hotspare/Makefile
+++ b/tests/sys/cddl/zfs/tests/hotspare/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/inheritance/Makefile b/tests/sys/cddl/zfs/tests/inheritance/Makefile
index b337fd541ed2..8dc20cb7b3bd 100644
--- a/tests/sys/cddl/zfs/tests/inheritance/Makefile
+++ b/tests/sys/cddl/zfs/tests/inheritance/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/interop/Makefile b/tests/sys/cddl/zfs/tests/interop/Makefile
index e81bcb332628..cd26031d634d 100644
--- a/tests/sys/cddl/zfs/tests/interop/Makefile
+++ b/tests/sys/cddl/zfs/tests/interop/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/inuse/Makefile b/tests/sys/cddl/zfs/tests/inuse/Makefile
index eb26734de6a5..81405a420e98 100644
--- a/tests/sys/cddl/zfs/tests/inuse/Makefile
+++ b/tests/sys/cddl/zfs/tests/inuse/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/iscsi/Makefile b/tests/sys/cddl/zfs/tests/iscsi/Makefile
index 523e83904d37..c531c390e4fe 100644
--- a/tests/sys/cddl/zfs/tests/iscsi/Makefile
+++ b/tests/sys/cddl/zfs/tests/iscsi/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/large_files/Makefile b/tests/sys/cddl/zfs/tests/large_files/Makefile
index b169b19e6e13..e18bf056e290 100644
--- a/tests/sys/cddl/zfs/tests/large_files/Makefile
+++ b/tests/sys/cddl/zfs/tests/large_files/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/largest_pool/Makefile b/tests/sys/cddl/zfs/tests/largest_pool/Makefile
index 4a5fdcbaae54..12cb14f3a264 100644
--- a/tests/sys/cddl/zfs/tests/largest_pool/Makefile
+++ b/tests/sys/cddl/zfs/tests/largest_pool/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/link_count/Makefile b/tests/sys/cddl/zfs/tests/link_count/Makefile
index 8bbdc98f20a6..fd9aab99f355 100644
--- a/tests/sys/cddl/zfs/tests/link_count/Makefile
+++ b/tests/sys/cddl/zfs/tests/link_count/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/migration/Makefile b/tests/sys/cddl/zfs/tests/migration/Makefile
index dae2827fe2f2..68c9977d34b7 100644
--- a/tests/sys/cddl/zfs/tests/migration/Makefile
+++ b/tests/sys/cddl/zfs/tests/migration/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/mmap/Makefile b/tests/sys/cddl/zfs/tests/mmap/Makefile
index aeeecf79170a..494b84b8e155 100644
--- a/tests/sys/cddl/zfs/tests/mmap/Makefile
+++ b/tests/sys/cddl/zfs/tests/mmap/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/mount/Makefile b/tests/sys/cddl/zfs/tests/mount/Makefile
index fbbdc22506a0..6b00ecf96c1a 100644
--- a/tests/sys/cddl/zfs/tests/mount/Makefile
+++ b/tests/sys/cddl/zfs/tests/mount/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/mv_files/Makefile b/tests/sys/cddl/zfs/tests/mv_files/Makefile
index 8409af69bf58..b81bf3fa49a9 100644
--- a/tests/sys/cddl/zfs/tests/mv_files/Makefile
+++ b/tests/sys/cddl/zfs/tests/mv_files/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/nestedfs/Makefile b/tests/sys/cddl/zfs/tests/nestedfs/Makefile
index c15cdb402e56..0ffdee46247a 100644
--- a/tests/sys/cddl/zfs/tests/nestedfs/Makefile
+++ b/tests/sys/cddl/zfs/tests/nestedfs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/no_space/Makefile b/tests/sys/cddl/zfs/tests/no_space/Makefile
index 69aac3b4adba..db55c2674165 100644
--- a/tests/sys/cddl/zfs/tests/no_space/Makefile
+++ b/tests/sys/cddl/zfs/tests/no_space/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/online_offline/Makefile b/tests/sys/cddl/zfs/tests/online_offline/Makefile
index a66642a57d85..5f0e68a5ccd5 100644
--- a/tests/sys/cddl/zfs/tests/online_offline/Makefile
+++ b/tests/sys/cddl/zfs/tests/online_offline/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/pool_names/Makefile b/tests/sys/cddl/zfs/tests/pool_names/Makefile
index 040f9ebd46e0..34350e28a800 100644
--- a/tests/sys/cddl/zfs/tests/pool_names/Makefile
+++ b/tests/sys/cddl/zfs/tests/pool_names/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/poolversion/Makefile b/tests/sys/cddl/zfs/tests/poolversion/Makefile
index 45ddce8deec3..6c04aac0f5fb 100644
--- a/tests/sys/cddl/zfs/tests/poolversion/Makefile
+++ b/tests/sys/cddl/zfs/tests/poolversion/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/quota/Makefile b/tests/sys/cddl/zfs/tests/quota/Makefile
index 36784d03ae82..beae44a4a503 100644
--- a/tests/sys/cddl/zfs/tests/quota/Makefile
+++ b/tests/sys/cddl/zfs/tests/quota/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/redundancy/Makefile b/tests/sys/cddl/zfs/tests/redundancy/Makefile
index cef8705f12e7..30345b04cc78 100644
--- a/tests/sys/cddl/zfs/tests/redundancy/Makefile
+++ b/tests/sys/cddl/zfs/tests/redundancy/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/refquota/Makefile b/tests/sys/cddl/zfs/tests/refquota/Makefile
index 3b9caf86c059..0b86cf46eaf5 100644
--- a/tests/sys/cddl/zfs/tests/refquota/Makefile
+++ b/tests/sys/cddl/zfs/tests/refquota/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/refreserv/Makefile b/tests/sys/cddl/zfs/tests/refreserv/Makefile
index 0c04cf3284b3..983cc768a338 100644
--- a/tests/sys/cddl/zfs/tests/refreserv/Makefile
+++ b/tests/sys/cddl/zfs/tests/refreserv/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/rename_dirs/Makefile b/tests/sys/cddl/zfs/tests/rename_dirs/Makefile
index ce111219676d..9aafbf759374 100644
--- a/tests/sys/cddl/zfs/tests/rename_dirs/Makefile
+++ b/tests/sys/cddl/zfs/tests/rename_dirs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/replacement/Makefile b/tests/sys/cddl/zfs/tests/replacement/Makefile
index e172a1f6ac60..f818166cdaa8 100644
--- a/tests/sys/cddl/zfs/tests/replacement/Makefile
+++ b/tests/sys/cddl/zfs/tests/replacement/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/reservation/Makefile b/tests/sys/cddl/zfs/tests/reservation/Makefile
index 781146bd2418..cda06d9c96c8 100644
--- a/tests/sys/cddl/zfs/tests/reservation/Makefile
+++ b/tests/sys/cddl/zfs/tests/reservation/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/tests/rootpool/Makefile b/tests/sys/cddl/zfs/tests/rootpool/Makefile
index c82ea9ab2927..b4ddcc9b3256 100644
--- a/tests/sys/cddl/zfs/tests/rootpool/Makefile
+++ b/tests/sys/cddl/zfs/tests/rootpool/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
diff --git a/tests/sys/cddl/zfs/tests/rsend/Makefile b/tests/sys/cddl/zfs/tests/rsend/Makefile
index 2d459ea74c9e..708b53ed90e7 100644
--- a/tests/sys/cddl/zfs/tests/rsend/Makefile
+++ b/tests/sys/cddl/zfs/tests/rsend/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile b/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile
index ae8f222f16a5..504f6210f6a8 100644
--- a/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile
+++ b/tests/sys/cddl/zfs/tests/scrub_mirror/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/slog/Makefile b/tests/sys/cddl/zfs/tests/slog/Makefile
index 69167acfc30c..0cdbca1395f3 100644
--- a/tests/sys/cddl/zfs/tests/slog/Makefile
+++ b/tests/sys/cddl/zfs/tests/slog/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/snapshot/Makefile b/tests/sys/cddl/zfs/tests/snapshot/Makefile
index 04275f3b2355..369dde3ba7ad 100644
--- a/tests/sys/cddl/zfs/tests/snapshot/Makefile
+++ b/tests/sys/cddl/zfs/tests/snapshot/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/snapused/Makefile b/tests/sys/cddl/zfs/tests/snapused/Makefile
index f65ecc1251a6..f831ac5dc699 100644
--- a/tests/sys/cddl/zfs/tests/snapused/Makefile
+++ b/tests/sys/cddl/zfs/tests/snapused/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/sparse/Makefile b/tests/sys/cddl/zfs/tests/sparse/Makefile
index df147d108320..c606a58e3b40 100644
--- a/tests/sys/cddl/zfs/tests/sparse/Makefile
+++ b/tests/sys/cddl/zfs/tests/sparse/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/threadsappend/Makefile b/tests/sys/cddl/zfs/tests/threadsappend/Makefile
index f62ad46f3777..fd179e27e715 100644
--- a/tests/sys/cddl/zfs/tests/threadsappend/Makefile
+++ b/tests/sys/cddl/zfs/tests/threadsappend/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/truncate/Makefile b/tests/sys/cddl/zfs/tests/truncate/Makefile
index 783456d96634..98c8e085408d 100644
--- a/tests/sys/cddl/zfs/tests/truncate/Makefile
+++ b/tests/sys/cddl/zfs/tests/truncate/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/txg_integrity/Makefile b/tests/sys/cddl/zfs/tests/txg_integrity/Makefile
index 010021572358..6a200b0c12b0 100644
--- a/tests/sys/cddl/zfs/tests/txg_integrity/Makefile
+++ b/tests/sys/cddl/zfs/tests/txg_integrity/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/userquota/Makefile b/tests/sys/cddl/zfs/tests/userquota/Makefile
index 6ec2a4f0b15c..678678ced1dd 100644
--- a/tests/sys/cddl/zfs/tests/userquota/Makefile
+++ b/tests/sys/cddl/zfs/tests/userquota/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/utils_test/Makefile b/tests/sys/cddl/zfs/tests/utils_test/Makefile
index 2b550b166a1f..e14bf4586580 100644
--- a/tests/sys/cddl/zfs/tests/utils_test/Makefile
+++ b/tests/sys/cddl/zfs/tests/utils_test/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/write_dirs/Makefile b/tests/sys/cddl/zfs/tests/write_dirs/Makefile
index ae7858b9772d..69d11aa30034 100644
--- a/tests/sys/cddl/zfs/tests/write_dirs/Makefile
+++ b/tests/sys/cddl/zfs/tests/write_dirs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/xattr/Makefile b/tests/sys/cddl/zfs/tests/xattr/Makefile
index 7a88513edc1c..a1f76ef79d72 100644
--- a/tests/sys/cddl/zfs/tests/xattr/Makefile
+++ b/tests/sys/cddl/zfs/tests/xattr/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zfsd/Makefile b/tests/sys/cddl/zfs/tests/zfsd/Makefile
index 7d3f29a7359e..e34e24b40906 100644
--- a/tests/sys/cddl/zfs/tests/zfsd/Makefile
+++ b/tests/sys/cddl/zfs/tests/zfsd/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib b/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib
index a4a7ef80ed9f..6369387ba069 100644
--- a/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib
+++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd.kshlib
@@ -52,7 +52,7 @@ function wait_for_pool_dev_state_change
function wait_for_pool_removal
{
typeset -i timeout=$1
- wait_for_pool_dev_state_change $timeout $REMOVAL_DISK "REMOVED|UNAVAIL"
+ wait_for_pool_dev_state_change $timeout $REMOVAL_DISK "REMOVED|UNAVAIL|FAULTED"
}
function wait_until_scrubbed
diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh
index 4eb04d60809e..7ad7a9113402 100644
--- a/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh
+++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_autoreplace_003_pos.ksh
@@ -83,19 +83,25 @@ typeset SPARE_NOP=${DISK4}.nop
typeset OTHER_DISKS="${DISK1} ${DISK2}"
typeset OTHER_NOPS=${OTHER_DISKS//~(E)([[:space:]]+|$)/.nop\1}
set -A MY_KEYWORDS "mirror" "raidz1" "raidz2"
+set -A MY_FAILURES "FAULTED" "REMOVED"
ensure_zfsd_running
log_must create_gnops $OTHER_DISKS $SPARE_DISK
-for keyword in "${MY_KEYWORDS[@]}" ; do
- log_must create_gnop $REMOVAL_DISK $PHYSPATH
- log_must create_pool $TESTPOOL $keyword $REMOVAL_NOP $OTHER_NOPS spare $SPARE_NOP
- log_must $ZPOOL set autoreplace=on $TESTPOOL
+for failure in "${MY_FAILURES[@]}" ; do
+ for keyword in "${MY_KEYWORDS[@]}" ; do
+ log_must create_gnop $REMOVAL_DISK $PHYSPATH
+ log_must create_pool $TESTPOOL $keyword $REMOVAL_NOP $OTHER_NOPS spare $SPARE_NOP
+ log_must $ZPOOL set autoreplace=on $TESTPOOL
- log_must destroy_gnop $REMOVAL_DISK
- log_must wait_for_pool_removal 20
- log_must create_gnop $NEW_DISK $PHYSPATH
- verify_assertion
- destroy_pool "$TESTPOOL"
- log_must destroy_gnop $NEW_DISK
+ if [ $failure = "FAULTED" ]; then
+ log_must zinject -d $REMOVAL_NOP -A fault $TESTPOOL
+ fi
+ log_must destroy_gnop $REMOVAL_DISK
+ log_must wait_for_pool_removal 20
+ log_must create_gnop $NEW_DISK $PHYSPATH
+ verify_assertion
+ destroy_pool "$TESTPOOL"
+ log_must destroy_gnop $NEW_DISK
+ done
done
log_pass
diff --git a/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh b/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh
index 3456a328e7f9..df704e183fb0 100644
--- a/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh
+++ b/tests/sys/cddl/zfs/tests/zfsd/zfsd_fault_001_pos.ksh
@@ -78,6 +78,10 @@ for type in "raidz" "mirror"; do
$DD if=/dev/zero bs=128k count=1 >> \
/$TESTPOOL/$TESTFS/$TESTFILE 2> /dev/null
$FSYNC /$TESTPOOL/$TESTFS/$TESTFILE
+ # Due to a bug outside of zfsd, it may be necessary to reopen
+ # the pool before it will become DEGRADED.
+ # https://github.com/openzfs/zfs/issues/16245
+ $ZPOOL reopen $TESTPOOL
# Check to see if the pool is faulted yet
$ZPOOL status $TESTPOOL | grep -q 'state: DEGRADED'
if [ $? == 0 ]
diff --git a/tests/sys/cddl/zfs/tests/zil/Makefile b/tests/sys/cddl/zfs/tests/zil/Makefile
index 387e799de35e..db30bfc9f6ec 100644
--- a/tests/sys/cddl/zfs/tests/zil/Makefile
+++ b/tests/sys/cddl/zfs/tests/zil/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zinject/Makefile b/tests/sys/cddl/zfs/tests/zinject/Makefile
index a317d61aa37a..6f37d7060abf 100644
--- a/tests/sys/cddl/zfs/tests/zinject/Makefile
+++ b/tests/sys/cddl/zfs/tests/zinject/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zones/Makefile b/tests/sys/cddl/zfs/tests/zones/Makefile
index d06e7a613aea..4b27462d1c0b 100644
--- a/tests/sys/cddl/zfs/tests/zones/Makefile
+++ b/tests/sys/cddl/zfs/tests/zones/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol/Makefile b/tests/sys/cddl/zfs/tests/zvol/Makefile
index deeb1b027b33..a0c4899ae106 100644
--- a/tests/sys/cddl/zfs/tests/zvol/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile
index 3f35f6b3ee8d..ffcc0f79e88c 100644
--- a/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol/zvol_ENOSPC/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile
index d6cb44734f17..4282538eecbc 100644
--- a/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol/zvol_cli/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile
index 19d944278c66..5f29f33a9b35 100644
--- a/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol/zvol_misc/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile b/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile
index 50d1f728638f..21acc05d8446 100644
--- a/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol/zvol_swap/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile b/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile
index bd3847db678e..71c8f330b156 100644
--- a/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile
+++ b/tests/sys/cddl/zfs/tests/zvol_thrash/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE=tests
diff --git a/tests/sys/common/Makefile b/tests/sys/common/Makefile
index 383fd17a5ba0..269aa9d52f9a 100644
--- a/tests/sys/common/Makefile
+++ b/tests/sys/common/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/common
${PACKAGE}FILES+= vnet.subr
diff --git a/tests/sys/common/vnet.subr b/tests/sys/common/vnet.subr
index 183f7284d614..bd98b02da33f 100644
--- a/tests/sys/common/vnet.subr
+++ b/tests/sys/common/vnet.subr
@@ -11,11 +11,32 @@ unlist_interface()
sed -i "" /^$1\$/d created_interfaces.lst
}
+_vnet_check_req()
+{
+ type=$1
+
+ if kldstat -q -n if_${type}.ko; then
+ return
+ fi
+
+ if ! kldload -n -q if_${type}; then
+ atf_skip "if_${type}.ko is required to run this test."
+ return
+ fi
+}
+
vnet_init()
{
if [ "`sysctl -i -n kern.features.vimage`" != 1 ]; then
atf_skip "This test requires VIMAGE"
fi
+
+ # Check if we can create if_epair or if_bridge interfaces.
+ # We may be running in a jail already, unable to load modules.
+ # If so, skip this test because it very likely (but not certainly)
+ # wants at least one of those
+ _vnet_check_req epair
+ _vnet_check_req bridge
}
vnet_mkepair()
@@ -26,6 +47,13 @@ vnet_mkepair()
echo ${ifname%a}
}
+vnet_init_bridge()
+{
+ if ! kldstat -q -m if_bridge; then
+ atf_skip "This test requires if_bridge"
+ fi
+}
+
vnet_mkbridge()
{
ifname=$(ifconfig bridge create)
diff --git a/tests/sys/compat32/Makefile b/tests/sys/compat32/Makefile
index 31834de16246..e1a35422410e 100644
--- a/tests/sys/compat32/Makefile
+++ b/tests/sys/compat32/Makefile
@@ -1,4 +1,3 @@
-
.if exists(${.CURDIR}/${MACHINE_ARCH})
SUBDIR+= ${MACHINE_ARCH}
.endif
diff --git a/tests/sys/compat32/Makefile.inc b/tests/sys/compat32/Makefile.inc
index 0220ac0431e9..7f870e3701fb 100644
--- a/tests/sys/compat32/Makefile.inc
+++ b/tests/sys/compat32/Makefile.inc
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/compat32
.include "../Makefile.inc"
diff --git a/tests/sys/devrandom/Makefile b/tests/sys/devrandom/Makefile
index e8a28789a7a3..342925246aee 100644
--- a/tests/sys/devrandom/Makefile
+++ b/tests/sys/devrandom/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
SDEVRANDOM= ${SRCTOP}/sys/dev/random
diff --git a/tests/sys/fifo/Makefile b/tests/sys/fifo/Makefile
index 766e9ee452a6..6d4c859288d9 100644
--- a/tests/sys/fifo/Makefile
+++ b/tests/sys/fifo/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/fifo
PLAIN_TESTS_C+= fifo_create
diff --git a/tests/sys/file/Makefile b/tests/sys/file/Makefile
index 010606bfeba0..f80d1b271b85 100644
--- a/tests/sys/file/Makefile
+++ b/tests/sys/file/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/file
BINDIR= ${TESTSDIR}
diff --git a/tests/sys/file/path_test.c b/tests/sys/file/path_test.c
index 911c7c7075f0..b3b8b7cebd4d 100644
--- a/tests/sys/file/path_test.c
+++ b/tests/sys/file/path_test.c
@@ -684,10 +684,14 @@ ATF_TC_BODY(path_io, tc)
size_t page_size;
int error, fd, pathfd, sd[2];
- /* It shouldn't be possible to create new files with O_PATH. */
+ /* It is allowed to create new files with O_PATH. */
snprintf(path, sizeof(path), "path_io.XXXXXX");
ATF_REQUIRE_MSG(mktemp(path) == path, FMT_ERR("mktemp"));
- ATF_REQUIRE_ERRNO(ENOENT, open(path, O_PATH | O_CREAT, 0600) < 0);
+ pathfd = open(path, O_PATH | O_CREAT, 0600);
+ ATF_REQUIRE_MSG(pathfd >= 0, FMT_ERR("open(O_PATH|O_CREAT)"));
+ /* Ensure that this is indeed O_PATH fd */
+ ATF_REQUIRE_ERRNO(EBADF, write(pathfd, path, strlen(path)) == -1);
+ CHECKED_CLOSE(pathfd);
/* Create a non-empty file for use in the rest of the tests. */
mktfile(path, "path_io.XXXXXX");
diff --git a/tests/sys/fs/Makefile b/tests/sys/fs/Makefile
index f0fd2cc5955f..e36a97d2335a 100644
--- a/tests/sys/fs/Makefile
+++ b/tests/sys/fs/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
.include <bsd.compiler.mk>
diff --git a/tests/sys/fs/Makefile.inc b/tests/sys/fs/Makefile.inc
index d3b5cbd3a79b..01b5f23410c8 100644
--- a/tests/sys/fs/Makefile.inc
+++ b/tests/sys/fs/Makefile.inc
@@ -1,2 +1 @@
-
.include "../Makefile.inc"
diff --git a/tests/sys/fs/fusefs/Makefile b/tests/sys/fs/fusefs/Makefile
index f45f2f93e1c0..b11f11bdfa98 100644
--- a/tests/sys/fs/fusefs/Makefile
+++ b/tests/sys/fs/fusefs/Makefile
@@ -1,10 +1,11 @@
-
.include <bsd.compiler.mk>
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/fs/fusefs
+ATF_TESTS_SH+= ctl
+
# We could simply link all of these files into a single executable. But since
# Kyua treats googletest programs as plain tests, it's better to separate them
# out, so we get more granular reporting.
@@ -40,6 +41,7 @@ GTESTS+= nfs
GTESTS+= notify
GTESTS+= open
GTESTS+= opendir
+GTESTS+= pre-init
GTESTS+= read
GTESTS+= readdir
GTESTS+= readlink
@@ -56,7 +58,6 @@ GTESTS+= xattr
.for p in ${GTESTS}
SRCS.$p+= ${p}.cc
-SRCS.$p+= getmntopts.c
SRCS.$p+= mockfs.cc
SRCS.$p+= utils.cc
.endfor
@@ -65,12 +66,13 @@ TEST_METADATA.default_permissions+= required_user="unprivileged"
TEST_METADATA.default_permissions_privileged+= required_user="root"
TEST_METADATA.mknod+= required_user="root"
TEST_METADATA.nfs+= required_user="root"
+# ctl must be exclusive because it disables/enables camsim
+TEST_METADATA.ctl+= is_exclusive="true"
+TEST_METADATA.ctl+= required_user="root"
-# TODO: drastically increase timeout after test development is mostly complete
TEST_METADATA+= timeout=10
FUSEFS= ${SRCTOP}/sys/fs/fuse
-MOUNT= ${SRCTOP}/sbin/mount
# Suppress warnings that GCC generates for the libc++ and gtest headers.
CXXWARNFLAGS.gcc+= -Wno-placement-new -Wno-attributes
# Suppress Wcast-align for readdir.cc, because it is unavoidable when using
@@ -89,9 +91,6 @@ CXXWARNFLAGS+= -Wno-vla-cxx-extension
.endif
CXXFLAGS+= -I${SRCTOP}/tests
CXXFLAGS+= -I${FUSEFS}
-CXXFLAGS+= -I${MOUNT}
-.PATH: ${MOUNT}
-CXXSTD= c++14
LIBADD+= pthread
LIBADD+= gmock gtest
diff --git a/tests/sys/fs/fusefs/allow_other.cc b/tests/sys/fs/fusefs/allow_other.cc
index dae6290ea8e5..24a161166a90 100644
--- a/tests/sys/fs/fusefs/allow_other.cc
+++ b/tests/sys/fs/fusefs/allow_other.cc
@@ -52,9 +52,6 @@ const static char RELPATH[] = "some_file.txt";
class NoAllowOther: public FuseTest {
public:
-/* Unprivileged user id */
-int m_uid;
-
virtual void SetUp() {
if (geteuid() != 0) {
GTEST_SKIP() << "This test must be run as root";
diff --git a/tests/sys/fs/fusefs/bmap.cc b/tests/sys/fs/fusefs/bmap.cc
index 4c9edac9360a..30612079657d 100644
--- a/tests/sys/fs/fusefs/bmap.cc
+++ b/tests/sys/fs/fusefs/bmap.cc
@@ -77,8 +77,6 @@ class BmapEof: public Bmap, public WithParamInterface<int> {};
/*
* Test FUSE_BMAP
- * XXX The FUSE protocol does not include the runp and runb variables, so those
- * must be guessed in-kernel.
*/
TEST_F(Bmap, bmap)
{
@@ -105,8 +103,19 @@ TEST_F(Bmap, bmap)
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, pbn);
- EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
- EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
+ /*
+ * XXX The FUSE protocol does not include the runp and runb variables,
+ * so those must be guessed in-kernel. There's no "right" answer, so
+ * just check that they're within reasonable limits.
+ */
+ EXPECT_LE(arg.runb, lbn);
+ EXPECT_LE((unsigned long)arg.runb, m_maxreadahead / m_maxbcachebuf);
+ EXPECT_LE((unsigned long)arg.runb, m_maxphys / m_maxbcachebuf);
+ EXPECT_GT(arg.runb, 0);
+ EXPECT_LE(arg.runp, filesize / m_maxbcachebuf - lbn);
+ EXPECT_LE((unsigned long)arg.runp, m_maxreadahead / m_maxbcachebuf);
+ EXPECT_LE((unsigned long)arg.runp, m_maxphys / m_maxbcachebuf);
+ EXPECT_GT(arg.runp, 0);
leak(fd);
}
@@ -142,7 +151,7 @@ TEST_F(Bmap, default_)
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, 0);
- EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1);
EXPECT_EQ(arg.runb, 0);
/* In the middle */
@@ -152,8 +161,8 @@ TEST_F(Bmap, default_)
arg.runb = -1;
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
- EXPECT_EQ(arg.runp, m_maxphys / m_maxbcachebuf - 1);
- EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ((unsigned long )arg.runp, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1);
/* Last block */
lbn = filesize / m_maxbcachebuf - 1;
@@ -163,7 +172,7 @@ TEST_F(Bmap, default_)
ASSERT_EQ(0, ioctl(fd, FIOBMAP2, &arg)) << strerror(errno);
EXPECT_EQ(arg.bn, lbn * m_maxbcachebuf / DEV_BSIZE);
EXPECT_EQ(arg.runp, 0);
- EXPECT_EQ(arg.runb, m_maxphys / m_maxbcachebuf - 1);
+ EXPECT_EQ((unsigned long )arg.runb, m_maxphys / m_maxbcachebuf - 1);
leak(fd);
}
diff --git a/tests/sys/fs/fusefs/ctl.sh b/tests/sys/fs/fusefs/ctl.sh
new file mode 100644
index 000000000000..7d2e7593cbdc
--- /dev/null
+++ b/tests/sys/fs/fusefs/ctl.sh
@@ -0,0 +1,69 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 ConnectWise
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
+# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+. $(atf_get_srcdir)/../../cam/ctl/ctl.subr
+
+# Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=283402
+#
+# Almost any fuse file system would work, but this tests uses fusefs-ext2
+# because it's simple and its download is very small.
+atf_test_case remove_lun_with_atime cleanup
+remove_lun_with_atime_head()
+{
+ atf_set "descr" "Remove a fuse-backed CTL LUN when atime is enabled"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "fuse-ext2 mkfs.ext2"
+}
+remove_lun_with_atime_body()
+{
+ MOUNTPOINT=$PWD/mnt
+ atf_check mkdir $MOUNTPOINT
+ atf_check truncate -s 1g ext2.img
+ atf_check mkfs.ext2 -q ext2.img
+ # Note: both default_permissions and atime must be enabled
+ atf_check fuse-ext2 -o default_permissions,allow_other,rw+ ext2.img \
+ $MOUNTPOINT
+
+ atf_check truncate -s 1m $MOUNTPOINT/file
+ create_block -o file=$MOUNTPOINT/file
+
+ # Force fusefs to open the file, and dirty its atime
+ atf_check dd if=/dev/$dev of=/dev/null count=1 status=none
+
+ # Finally, remove the LUN. Hopefully it won't panic.
+ atf_check -o ignore ctladm remove -b block -l $LUN
+
+ rm lun-create.txt # So we don't try to remove the LUN twice
+}
+remove_lun_with_atime_cleanup()
+{
+ cleanup
+ umount $PWD/mnt
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case remove_lun_with_atime
+}
diff --git a/tests/sys/fs/fusefs/destroy.cc b/tests/sys/fs/fusefs/destroy.cc
index 16d50da19b9b..45acb1f99724 100644
--- a/tests/sys/fs/fusefs/destroy.cc
+++ b/tests/sys/fs/fusefs/destroy.cc
@@ -60,7 +60,7 @@ static void* open_th(void* arg) {
* Check for any memory leaks like this:
* 1) kldunload fusefs, if necessary
* 2) kldload fusefs
- * 3) ./destroy --gtest_filter=Destroy.unsent_operations
+ * 3) ./destroy --gtest_filter=Death.unsent_operations
* 4) kldunload fusefs
* 5) check /var/log/messages for anything like this:
Freed UMA keg (fuse_ticket) was not empty (31 items). Lost 2 pages of memory.
diff --git a/tests/sys/fs/fusefs/fallocate.cc b/tests/sys/fs/fusefs/fallocate.cc
index ff5e3eb4f4bb..4e5b047b78b7 100644
--- a/tests/sys/fs/fusefs/fallocate.cc
+++ b/tests/sys/fs/fusefs/fallocate.cc
@@ -32,10 +32,9 @@ extern "C" {
#include <sys/time.h>
#include <fcntl.h>
+#include <mntopts.h> // for build_iovec
#include <signal.h>
#include <unistd.h>
-
-#include "mntopts.h" // for build_iovec
}
#include "mockfs.hh"
@@ -310,6 +309,57 @@ TEST_F(Fspacectl, erofs)
leak(fd);
}
+/*
+ * If FUSE_GETATTR fails when determining the size of the file, fspacectl
+ * should fail gracefully. This failure mode is easiest to trigger when
+ * attribute caching is disabled.
+ */
+TEST_F(Fspacectl, getattr_fails)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ Sequence seq;
+ struct spacectl_range rqsr;
+ const uint64_t ino = 42;
+ const uint64_t fsize = 2000;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1, 0);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(1)
+ .InSequence(seq)
+ .WillOnce(Invoke(ReturnImmediate([](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, attr);
+ out.body.attr.attr.ino = ino;
+ out.body.attr.attr.mode = S_IFREG | 0644;
+ out.body.attr.attr.size = fsize;
+ out.body.attr.attr_valid = 0;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_GETATTR &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).InSequence(seq)
+ .WillOnce(ReturnErrno(EIO));
+
+ fd = open(FULLPATH, O_RDWR);
+ ASSERT_LE(0, fd) << strerror(errno);
+ rqsr.r_offset = 500;
+ rqsr.r_len = 1000;
+ EXPECT_EQ(-1, fspacectl(fd, SPACECTL_DEALLOC, &rqsr, 0, NULL));
+ EXPECT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
TEST_F(Fspacectl, ok)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
diff --git a/tests/sys/fs/fusefs/flush.cc b/tests/sys/fs/fusefs/flush.cc
index 474cdbdb2203..7ba1218b3287 100644
--- a/tests/sys/fs/fusefs/flush.cc
+++ b/tests/sys/fs/fusefs/flush.cc
@@ -109,6 +109,36 @@ TEST_F(Flush, open_twice)
EXPECT_EQ(0, close(fd)) << strerror(errno);
}
+/**
+ * Test for FOPEN_NOFLUSH: we expect that zero flush calls will be performed.
+ */
+TEST_F(Flush, open_noflush)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ uint64_t pid = (uint64_t)getpid();
+ int fd;
+
+ expect_lookup(RELPATH, ino, 1);
+ expect_open(ino, FOPEN_NOFLUSH, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_FLUSH &&
+ in.header.nodeid == ino &&
+ in.body.flush.lock_owner == pid &&
+ in.body.flush.fh == FH);
+ }, Eq(true)),
+ _)
+ ).Times(0);
+ expect_release();
+
+ fd = open(FULLPATH, O_WRONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+ // close MUST not flush
+ EXPECT_EQ(0, close(fd)) << strerror(errno);
+}
+
/*
* Some FUSE filesystem cache data internally and flush it on release. Such
* filesystems may generate errors during release. On Linux, these get
diff --git a/tests/sys/fs/fusefs/forget.cc b/tests/sys/fs/fusefs/forget.cc
index 846198e75925..1e7764ac4782 100644
--- a/tests/sys/fs/fusefs/forget.cc
+++ b/tests/sys/fs/fusefs/forget.cc
@@ -31,7 +31,6 @@
extern "C" {
#include <sys/types.h>
#include <sys/mount.h>
-#include <sys/sysctl.h>
#include <fcntl.h>
#include <semaphore.h>
diff --git a/tests/sys/fs/fusefs/io.cc b/tests/sys/fs/fusefs/io.cc
index 99b5eae34e09..ced291836da0 100644
--- a/tests/sys/fs/fusefs/io.cc
+++ b/tests/sys/fs/fusefs/io.cc
@@ -31,13 +31,14 @@
extern "C" {
#include <sys/types.h>
#include <sys/mman.h>
-#include <sys/sysctl.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
}
+#include <iomanip>
+
#include "mockfs.hh"
#include "utils.hh"
diff --git a/tests/sys/fs/fusefs/last_local_modify.cc b/tests/sys/fs/fusefs/last_local_modify.cc
index 495bfd8aa959..5fcd3c36c892 100644
--- a/tests/sys/fs/fusefs/last_local_modify.cc
+++ b/tests/sys/fs/fusefs/last_local_modify.cc
@@ -233,7 +233,6 @@ TEST_P(LastLocalModify, lookup)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
- out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
@@ -277,6 +276,7 @@ TEST_P(LastLocalModify, lookup)
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.entry.attr.mode = S_IFREG | 0644;
out0->body.entry.nodeid = ino;
+ out0->body.entry.attr.ino = ino;
out0->body.entry.entry_valid = UINT64_MAX;
out0->body.entry.attr_valid = UINT64_MAX;
out0->body.entry.attr.size = oldsize;
@@ -392,7 +392,6 @@ TEST_P(LastLocalModify, vfs_vget)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
- out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
@@ -414,7 +413,6 @@ TEST_P(LastLocalModify, vfs_vget)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.nodeid = ino;
out.body.entry.attr.size = oldsize;
- out.body.entry.nodeid = ino;
out.body.entry.attr_valid_nsec = NAP_NS / 2;
out.body.entry.attr.ino = ino;
out.body.entry.attr.mode = S_IFREG | 0644;
@@ -439,6 +437,7 @@ TEST_P(LastLocalModify, vfs_vget)
SET_OUT_HEADER_LEN(*out0, entry);
out0->body.entry.attr.mode = S_IFREG | 0644;
out0->body.entry.nodeid = ino;
+ out0->body.entry.attr.ino = ino;
out0->body.entry.entry_valid = UINT64_MAX;
out0->body.entry.attr_valid = UINT64_MAX;
out0->body.entry.attr.size = oldsize;
diff --git a/tests/sys/fs/fusefs/lookup.cc b/tests/sys/fs/fusefs/lookup.cc
index 6d506c1ab700..2cfe888b6b08 100644
--- a/tests/sys/fs/fusefs/lookup.cc
+++ b/tests/sys/fs/fusefs/lookup.cc
@@ -560,6 +560,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0; // immediate timeout
})));
@@ -568,6 +569,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr.ino = bar_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -577,6 +579,7 @@ TEST_F(LookupExportable, dotdot_entry_cache_timeout)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = FUSE_ROOT_ID;
+ out.body.entry.attr.ino = FUSE_ROOT_ID;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -607,6 +610,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -615,6 +619,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = bar_ino;
+ out.body.entry.attr.ino = bar_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -632,6 +637,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = foo_ino;
+ out.body.entry.attr.ino = foo_ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -640,6 +646,7 @@ TEST_F(LookupExportable, dotdot_no_parent_nid)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = FUSE_ROOT_ID;
+ out.body.entry.attr.ino = FUSE_ROOT_ID;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
diff --git a/tests/sys/fs/fusefs/lseek.cc b/tests/sys/fs/fusefs/lseek.cc
index 5ffeb4b33cbd..12d41f7af1b2 100644
--- a/tests/sys/fs/fusefs/lseek.cc
+++ b/tests/sys/fs/fusefs/lseek.cc
@@ -71,6 +71,7 @@ TEST_F(LseekPathconf, already_enosys)
).WillOnce(Invoke(ReturnErrno(ENOSYS)));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(offset_in, lseek(fd, offset_in, SEEK_DATA));
EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
@@ -105,6 +106,7 @@ TEST_F(LseekPathconf, already_seeked)
out.body.lseek.offset = i.body.lseek.offset;
})));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(offset, lseek(fd, offset, SEEK_DATA));
EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
@@ -113,6 +115,76 @@ TEST_F(LseekPathconf, already_seeked)
}
/*
+ * Use pathconf on a file not already opened. The server returns EACCES when
+ * the kernel tries to open it. The kernel should return EACCES, and make no
+ * judgement about whether the server does or does not support FUSE_LSEEK.
+ */
+TEST_F(LseekPathconf, eacces)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.entry_valid = UINT64_MAX;
+ out.body.entry.attr.mode = S_IFREG | 0644;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.size = fsize;
+ })));
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_OPEN &&
+ in.header.nodeid == ino);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EACCES)));
+
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EACCES, errno);
+}
+
+/*
+ * If the server returns some weird error when we try FUSE_LSEEK, send that to
+ * the caller but don't record the answer.
+ */
+TEST_F(LseekPathconf, eio)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).Times(2)
+ .WillRepeatedly(Invoke(ReturnErrno(EIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+ /* Check again, to ensure that the kernel didn't record the response */
+ EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ EXPECT_EQ(EIO, errno);
+
+ leak(fd);
+}
+
+/*
* If no FUSE_LSEEK operation has been attempted since mount, try once as soon
* as a pathconf request comes in.
*/
@@ -134,6 +206,7 @@ TEST_F(LseekPathconf, enosys_now)
).WillOnce(Invoke(ReturnErrno(ENOSYS)));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
EXPECT_EQ(EINVAL, errno);
@@ -142,6 +215,34 @@ TEST_F(LseekPathconf, enosys_now)
}
/*
+ * Use pathconf, rather than fpathconf, on a file not already opened.
+ * Regression test for https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=278135
+ */
+TEST_F(LseekPathconf, pathconf)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 1 << 30; /* 1 GiB */
+ off_t offset_out = 1 << 29;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([=](auto i __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, lseek);
+ out.body.lseek.offset = offset_out;
+ })));
+ expect_release(ino, FuseTest::FH);
+
+ EXPECT_EQ(1, pathconf(FULLPATH, _PC_MIN_HOLE_SIZE)) << strerror(errno);
+}
+
+/*
* If no FUSE_LSEEK operation has been attempted since mount, try one as soon
* as a pathconf request comes in. This is the typical pattern of bsdtar. It
* will only try SEEK_HOLE/SEEK_DATA if fpathconf says they're supported.
@@ -169,6 +270,7 @@ TEST_F(LseekPathconf, seek_now)
})));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(offset_initial, lseek(fd, offset_initial, SEEK_SET));
EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
/* And check that the file pointer hasn't changed */
@@ -178,6 +280,39 @@ TEST_F(LseekPathconf, seek_now)
}
/*
+ * If the user calls pathconf(_, _PC_MIN_HOLE_SIZE) on a fully sparse or
+ * zero-length file, then SEEK_DATA will return ENXIO. That should be
+ * interpreted as success.
+ */
+TEST_F(LseekPathconf, zerolength)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ const uint64_t ino = 42;
+ off_t fsize = 0;
+ int fd;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, fsize, 1);
+ expect_open(ino, 0, 1);
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ return (in.header.opcode == FUSE_LSEEK &&
+ in.header.nodeid == ino &&
+ in.body.lseek.whence == SEEK_DATA);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnErrno(ENXIO)));
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+ /* Check again, to ensure that the kernel recorded the response */
+ EXPECT_EQ(1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
+
+ leak(fd);
+}
+
+/*
* For servers using older protocol versions, no FUSE_LSEEK should be attempted
*/
TEST_F(LseekPathconf_7_23, already_enosys)
@@ -198,6 +333,7 @@ TEST_F(LseekPathconf_7_23, already_enosys)
).Times(0);
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(-1, fpathconf(fd, _PC_MIN_HOLE_SIZE));
EXPECT_EQ(EINVAL, errno);
@@ -262,6 +398,7 @@ TEST_F(LseekSeekData, enosys)
_)
).WillOnce(Invoke(ReturnErrno(ENOSYS)));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
/*
* Default behavior: ENXIO if offset is < 0 or >= fsize, offset
@@ -302,6 +439,7 @@ TEST_F(LseekSeekHole, ok)
out.body.lseek.offset = offset_out;
})));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(offset_out, lseek(fd, offset_in, SEEK_HOLE));
EXPECT_EQ(offset_out, lseek(fd, 0, SEEK_CUR));
@@ -334,6 +472,7 @@ TEST_F(LseekSeekHole, enosys)
_)
).WillOnce(Invoke(ReturnErrno(ENOSYS)));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
/*
* Default behavior: ENXIO if offset is < 0 or >= fsize, fsize
@@ -371,6 +510,7 @@ TEST_F(LseekSeekHole, enxio)
_)
).WillOnce(Invoke(ReturnErrno(ENXIO)));
fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd);
EXPECT_EQ(-1, lseek(fd, offset_in, SEEK_HOLE));
EXPECT_EQ(ENXIO, errno);
diff --git a/tests/sys/fs/fusefs/mknod.cc b/tests/sys/fs/fusefs/mknod.cc
index 1fb855f44f29..eb745f19acd5 100644
--- a/tests/sys/fs/fusefs/mknod.cc
+++ b/tests/sys/fs/fusefs/mknod.cc
@@ -283,7 +283,7 @@ TEST_F(Mknod, parent_inode)
}
/*
- * fusefs(5) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a
+ * fusefs(4) lacks VOP_WHITEOUT support. No bugzilla entry, because that's a
* feature, not a bug
*/
TEST_F(Mknod, DISABLED_whiteout)
diff --git a/tests/sys/fs/fusefs/mockfs.cc b/tests/sys/fs/fusefs/mockfs.cc
index bd7bd1b663f9..65cdc3919652 100644
--- a/tests/sys/fs/fusefs/mockfs.cc
+++ b/tests/sys/fs/fusefs/mockfs.cc
@@ -39,13 +39,12 @@ extern "C" {
#include <fcntl.h>
#include <libutil.h>
+#include <mntopts.h> // for build_iovec
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
-
-#include "mntopts.h" // for build_iovec
}
#include <cinttypes>
@@ -416,11 +415,25 @@ void MockFS::debug_response(const mockfs_buf_out &out) {
}
}
-MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
+MockFS::MockFS(int max_read, int max_readahead, bool allow_other,
+ bool default_permissions,
bool push_symlinks_in, bool ro, enum poll_method pm, uint32_t flags,
uint32_t kernel_minor_version, uint32_t max_write, bool async,
bool noclusterr, unsigned time_gran, bool nointr, bool noatime,
- const char *fsname, const char *subtype)
+ const char *fsname, const char *subtype, bool no_auto_init)
+ : m_daemon_id(NULL),
+ m_kernel_minor_version(kernel_minor_version),
+ m_kq(pm == KQ ? kqueue() : -1),
+ m_maxread(max_read),
+ m_maxreadahead(max_readahead),
+ m_pid(getpid()),
+ m_uniques(new std::unordered_set<uint64_t>),
+ m_pm(pm),
+ m_time_gran(time_gran),
+ m_child_pid(-1),
+ m_maxwrite(MIN(max_write, max_max_write)),
+ m_nready(-1),
+ m_quit(false)
{
struct sigaction sa;
struct iovec *iov = NULL;
@@ -428,20 +441,6 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
char fdstr[15];
const bool trueval = true;
- m_daemon_id = NULL;
- m_kernel_minor_version = kernel_minor_version;
- m_maxreadahead = max_readahead;
- m_maxwrite = MIN(max_write, max_max_write);
- m_nready = -1;
- m_pm = pm;
- m_time_gran = time_gran;
- m_quit = false;
- m_last_unique = 0;
- if (m_pm == KQ)
- m_kq = kqueue();
- else
- m_kq = -1;
-
/*
* Kyua sets pwd to a testcase-unique tempdir; no need to use
* mkdtemp
@@ -466,15 +465,18 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
throw(std::system_error(errno, std::system_category(),
"Couldn't open /dev/fuse"));
- m_pid = getpid();
- m_child_pid = -1;
-
build_iovec(&iov, &iovlen, "fstype", __DECONST(void *, "fusefs"), -1);
build_iovec(&iov, &iovlen, "fspath",
__DECONST(void *, "mountpoint"), -1);
build_iovec(&iov, &iovlen, "from", __DECONST(void *, "/dev/fuse"), -1);
sprintf(fdstr, "%d", m_fuse_fd);
build_iovec(&iov, &iovlen, "fd", fdstr, -1);
+ if (m_maxread > 0) {
+ char val[10];
+
+ snprintf(val, sizeof(val), "%d", m_maxread);
+ build_iovec(&iov, &iovlen, "max_read=", &val, -1);
+ }
if (allow_other) {
build_iovec(&iov, &iovlen, "allow_other",
__DECONST(void*, &trueval), sizeof(bool));
@@ -527,7 +529,9 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
ON_CALL(*this, process(_, _))
.WillByDefault(Invoke(this, &MockFS::process_default));
- init(flags);
+ if (!no_auto_init)
+ init(flags);
+
bzero(&sa, sizeof(sa));
sa.sa_handler = sigint_handler;
sa.sa_flags = 0; /* Don't set SA_RESTART! */
@@ -541,10 +545,7 @@ MockFS::MockFS(int max_readahead, bool allow_other, bool default_permissions,
MockFS::~MockFS() {
kill_daemon();
- if (m_daemon_id != NULL) {
- pthread_join(m_daemon_id, NULL);
- m_daemon_id = NULL;
- }
+ join_daemon();
::unmount("mountpoint", MNT_FORCE);
rmdir("mountpoint");
if (m_kq >= 0)
@@ -738,14 +739,10 @@ void MockFS::audit_request(const mockfs_buf_in &in, ssize_t buflen) {
default:
FAIL() << "Unknown opcode " << in.header.opcode;
}
- /*
- * Check that the ticket's unique value is sequential. Technically it
- * doesn't need to be sequential, merely unique. But the current
- * fusefs driver _does_ make it sequential, and that's easy to check
- * for.
- */
- if (in.header.unique != ++m_last_unique)
- FAIL() << "Non-sequential unique value";
+ /* Verify that the ticket's unique value is actually unique. */
+ if (m_uniques->find(in.header.unique) != m_uniques->end())
+ FAIL() << "Non-unique \"unique\" value";
+ m_uniques->insert(in.header.unique);
}
void MockFS::init(uint32_t flags) {
@@ -789,6 +786,13 @@ void MockFS::kill_daemon() {
m_fuse_fd = -1;
}
+void MockFS::join_daemon() {
+ if (m_daemon_id != NULL) {
+ pthread_join(m_daemon_id, NULL);
+ m_daemon_id = NULL;
+ }
+}
+
void MockFS::loop() {
std::vector<std::unique_ptr<mockfs_buf_out>> out;
@@ -1035,6 +1039,10 @@ void MockFS::write_response(const mockfs_buf_out &out) {
ASSERT_EQ(-1, r);
ASSERT_EQ(out.expected_errno, errno) << strerror(errno);
} else {
+ if (r <= 0 && errno == EINVAL) {
+ printf("Failed to write response. unique=%" PRIu64
+ ":\n", out.header.unique);
+ }
ASSERT_TRUE(r > 0 || errno == EAGAIN) << strerror(errno);
}
}
diff --git a/tests/sys/fs/fusefs/mockfs.hh b/tests/sys/fs/fusefs/mockfs.hh
index 958964f769d4..ba6f7fded9d0 100644
--- a/tests/sys/fs/fusefs/mockfs.hh
+++ b/tests/sys/fs/fusefs/mockfs.hh
@@ -36,6 +36,8 @@ extern "C" {
#include "fuse_kernel.h"
}
+#include <unordered_set>
+
#include <gmock/gmock.h>
#define TIME_T_MAX (std::numeric_limits<time_t>::max())
@@ -292,14 +294,20 @@ class MockFS {
int m_kq;
+ /*
+ * If nonzero, the maximum size in bytes of a read that the kernel will
+ * send to the server.
+ */
+ int m_maxread;
+
/* The max_readahead file system option */
uint32_t m_maxreadahead;
/* pid of the test process */
pid_t m_pid;
- /* The unique value of the header of the last received operation */
- uint64_t m_last_unique;
+ /* Every "unique" value of a fuse ticket seen so far */
+ std::unique_ptr<std::unordered_set<uint64_t>> m_uniques;
/* Method the daemon should use for I/O to and from /dev/fuse */
enum poll_method m_pm;
@@ -353,18 +361,22 @@ class MockFS {
bool m_quit;
/* Create a new mockfs and mount it to a tempdir */
- MockFS(int max_readahead, bool allow_other,
+ MockFS(int max_read, int max_readahead, bool allow_other,
bool default_permissions, bool push_symlinks_in, bool ro,
enum poll_method pm, uint32_t flags,
uint32_t kernel_minor_version, uint32_t max_write, bool async,
bool no_clusterr, unsigned time_gran, bool nointr,
- bool noatime, const char *fsname, const char *subtype);
+ bool noatime, const char *fsname, const char *subtype,
+ bool no_auto_init);
virtual ~MockFS();
/* Kill the filesystem daemon without unmounting the filesystem */
void kill_daemon();
+ /* Wait until the daemon thread terminates */
+ void join_daemon();
+
/* Process FUSE requests endlessly */
void loop();
diff --git a/tests/sys/fs/fusefs/mount.cc b/tests/sys/fs/fusefs/mount.cc
index 7a8d2c1396f0..ece518b09f66 100644
--- a/tests/sys/fs/fusefs/mount.cc
+++ b/tests/sys/fs/fusefs/mount.cc
@@ -33,7 +33,7 @@ extern "C" {
#include <sys/mount.h>
#include <sys/uio.h>
-#include "mntopts.h" // for build_iovec
+#include <mntopts.h> // for build_iovec
}
#include "mockfs.hh"
diff --git a/tests/sys/fs/fusefs/nfs.cc b/tests/sys/fs/fusefs/nfs.cc
index 79fead8e77cb..2fa2b290f383 100644
--- a/tests/sys/fs/fusefs/nfs.cc
+++ b/tests/sys/fs/fusefs/nfs.cc
@@ -84,6 +84,7 @@ TEST_F(Fhstat, estale)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0;
@@ -95,6 +96,7 @@ TEST_F(Fhstat, estale)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 2;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0;
@@ -121,6 +123,7 @@ TEST_F(Fhstat, lookup_dot)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr.uid = uid;
out.body.entry.attr_valid = UINT64_MAX;
@@ -132,6 +135,7 @@ TEST_F(Fhstat, lookup_dot)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr.uid = uid;
out.body.entry.attr_valid = UINT64_MAX;
@@ -144,6 +148,37 @@ TEST_F(Fhstat, lookup_dot)
EXPECT_EQ(mode, sb.st_mode);
}
+/* Gracefully handle failures to lookup ".". */
+TEST_F(Fhstat, lookup_dot_error)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino = 42;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.attr.mode = mode;
+ out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino, ".")
+ .WillOnce(Invoke(ReturnErrno(EDOOFUS)));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ ASSERT_EQ(-1, fhstat(&fhp, &sb));
+ EXPECT_EQ(EDOOFUS, errno);
+}
+
/* Use a file handle whose entry is still cached */
TEST_F(Fhstat, cached)
{
@@ -159,6 +194,7 @@ TEST_F(Fhstat, cached)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr.ino = ino;
out.body.entry.attr_valid = UINT64_MAX;
@@ -185,6 +221,7 @@ TEST_F(Fhstat, cache_expired)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr.ino = ino;
out.body.entry.attr_valid = UINT64_MAX;
@@ -196,6 +233,7 @@ TEST_F(Fhstat, cache_expired)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr.ino = ino;
out.body.entry.attr_valid = UINT64_MAX;
@@ -213,6 +251,99 @@ TEST_F(Fhstat, cache_expired)
EXPECT_EQ(ino, sb.st_ino);
}
+/*
+ * If the server returns a FUSE_LOOKUP response for a nodeid that we didn't
+ * lookup, it's a bug. But we should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_nodeid)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t ino_in = 42;
+ const uint64_t ino_out = 43;
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_in;
+ out.body.entry.attr.ino = ino_in;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(ino_in, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = ino_out;
+ out.body.entry.attr.ino = ino_out;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ EXPECT_NE(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(EIO, errno);
+}
+
+/*
+ * If the server returns a FUSE_LOOKUP response where the nodeid doesn't match
+ * the inode number, and the file system is exported, it's a bug. But we
+ * should handle it gracefully.
+ */
+TEST_F(Fhstat, inconsistent_ino)
+{
+ const char FULLPATH[] = "mountpoint/some_dir/.";
+ const char RELDIRPATH[] = "some_dir";
+ fhandle_t fhp;
+ struct stat sb;
+ const uint64_t nodeid = 42;
+ const uint64_t ino = 711; // Could be anything that != nodeid
+ const mode_t mode = S_IFDIR | 0755;
+ const uid_t uid = 12345;
+
+ EXPECT_LOOKUP(FUSE_ROOT_ID, RELDIRPATH)
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = nodeid;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ EXPECT_LOOKUP(nodeid, ".")
+ .WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+ SET_OUT_HEADER_LEN(out, entry);
+ out.body.entry.nodeid = nodeid;
+ out.body.entry.attr.ino = ino;
+ out.body.entry.attr.mode = mode;
+ out.body.entry.generation = 1;
+ out.body.entry.attr.uid = uid;
+ out.body.entry.attr_valid = UINT64_MAX;
+ out.body.entry.entry_valid = 0;
+ })));
+
+ ASSERT_EQ(0, getfh(FULLPATH, &fhp)) << strerror(errno);
+ /*
+ * The fhstat operation will actually succeed. But future operations
+ * will likely fail.
+ */
+ ASSERT_EQ(0, fhstat(&fhp, &sb)) << strerror(errno);
+ EXPECT_EQ(ino, sb.st_ino);
+}
+
/*
* If the server doesn't set FUSE_EXPORT_SUPPORT, then we can't do NFS-style
* lookups
@@ -230,6 +361,7 @@ TEST_F(FhstatNotExportable, lookup_dot)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0;
@@ -252,6 +384,7 @@ TEST_F(Getfh, eoverflow)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = (uint64_t)UINT32_MAX + 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
@@ -274,6 +407,7 @@ TEST_F(Getfh, ok)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = S_IFDIR | 0755;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = UINT64_MAX;
})));
@@ -305,6 +439,7 @@ TEST_F(Readdir, getdirentries)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0;
@@ -315,6 +450,7 @@ TEST_F(Readdir, getdirentries)
SET_OUT_HEADER_LEN(out, entry);
out.body.entry.attr.mode = mode;
out.body.entry.nodeid = ino;
+ out.body.entry.attr.ino = ino;
out.body.entry.generation = 1;
out.body.entry.attr_valid = UINT64_MAX;
out.body.entry.entry_valid = 0;
diff --git a/tests/sys/fs/fusefs/notify.cc b/tests/sys/fs/fusefs/notify.cc
index e3f539f57599..1e22bde13db7 100644
--- a/tests/sys/fs/fusefs/notify.cc
+++ b/tests/sys/fs/fusefs/notify.cc
@@ -30,7 +30,6 @@
extern "C" {
#include <sys/types.h>
-#include <sys/sysctl.h>
#include <fcntl.h>
#include <pthread.h>
diff --git a/tests/sys/fs/fusefs/open.cc b/tests/sys/fs/fusefs/open.cc
index 7ab3aeb6ba2a..1212a7047f26 100644
--- a/tests/sys/fs/fusefs/open.cc
+++ b/tests/sys/fs/fusefs/open.cc
@@ -70,16 +70,8 @@ void test_ok(int os_flags, int fuse_flags) {
}
};
-
-class OpenNoOpenSupport: public FuseTest {
- virtual void SetUp() {
- m_init_flags = FUSE_NO_OPEN_SUPPORT;
- FuseTest::SetUp();
- }
-};
-
/*
- * fusefs(5) does not support I/O on device nodes (neither does UFS). But it
+ * fusefs(4) does not support I/O on device nodes (neither does UFS). But it
* shouldn't crash
*/
TEST_F(Open, chr)
@@ -281,37 +273,11 @@ TEST_F(Open, o_rdwr)
}
/*
- * Without FUSE_NO_OPEN_SUPPORT, returning ENOSYS is an error
- */
-TEST_F(Open, enosys)
-{
- const char FULLPATH[] = "mountpoint/some_file.txt";
- const char RELPATH[] = "some_file.txt";
- uint64_t ino = 42;
- int fd;
-
- FuseTest::expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
- EXPECT_CALL(*m_mock, process(
- ResultOf([=](auto in) {
- return (in.header.opcode == FUSE_OPEN &&
- in.body.open.flags == (uint32_t)O_RDONLY &&
- in.header.nodeid == ino);
- }, Eq(true)),
- _)
- ).Times(1)
- .WillOnce(Invoke(ReturnErrno(ENOSYS)));
-
- fd = open(FULLPATH, O_RDONLY);
- ASSERT_EQ(-1, fd) << strerror(errno);
- EXPECT_EQ(ENOSYS, errno);
-}
-
-/*
- * If a fuse server sets FUSE_NO_OPEN_SUPPORT and returns ENOSYS to a
+ * If a fuse server returns ENOSYS to a
* FUSE_OPEN, then it and subsequent FUSE_OPEN and FUSE_RELEASE operations will
* also succeed automatically without being sent to the server.
*/
-TEST_F(OpenNoOpenSupport, enosys)
+TEST_F(Open, enosys)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
diff --git a/tests/sys/fs/fusefs/opendir.cc b/tests/sys/fs/fusefs/opendir.cc
index dd837a8d43c1..e1fed59635fc 100644
--- a/tests/sys/fs/fusefs/opendir.cc
+++ b/tests/sys/fs/fusefs/opendir.cc
@@ -71,13 +71,6 @@ void expect_opendir(uint64_t ino, uint32_t flags, ProcessMockerT r)
};
-class OpendirNoOpendirSupport: public Opendir {
- virtual void SetUp() {
- m_init_flags = FUSE_NO_OPENDIR_SUPPORT;
- FuseTest::SetUp();
- }
-};
-
/*
* The fuse daemon fails the request with enoent. This usually indicates a
@@ -179,27 +172,11 @@ TEST_F(Opendir, opendir)
}
/*
- * Without FUSE_NO_OPENDIR_SUPPORT, returning ENOSYS is an error
- */
-TEST_F(Opendir, enosys)
-{
- const char FULLPATH[] = "mountpoint/some_file.txt";
- const char RELPATH[] = "some_file.txt";
- uint64_t ino = 42;
-
- expect_lookup(RELPATH, ino);
- expect_opendir(ino, O_RDONLY, ReturnErrno(ENOSYS));
-
- EXPECT_EQ(-1, open(FULLPATH, O_DIRECTORY));
- EXPECT_EQ(ENOSYS, errno);
-}
-
-/*
- * If a fuse server sets FUSE_NO_OPENDIR_SUPPORT and returns ENOSYS to a
+ * If a fuse server returns ENOSYS to a
* FUSE_OPENDIR, then it and subsequent FUSE_OPENDIR and FUSE_RELEASEDIR
* operations will also succeed automatically without being sent to the server.
*/
-TEST_F(OpendirNoOpendirSupport, enosys)
+TEST_F(Opendir, enosys)
{
const char FULLPATH[] = "mountpoint/some_file.txt";
const char RELPATH[] = "some_file.txt";
diff --git a/tests/sys/fs/fusefs/pre-init.cc b/tests/sys/fs/fusefs/pre-init.cc
new file mode 100644
index 000000000000..e990d3cafffa
--- /dev/null
+++ b/tests/sys/fs/fusefs/pre-init.cc
@@ -0,0 +1,154 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 ConnectWise
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+extern "C" {
+#include <sys/param.h>
+#include <sys/mount.h>
+#include <sys/signal.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <semaphore.h>
+#include <signal.h>
+}
+
+#include "mockfs.hh"
+#include "utils.hh"
+
+using namespace testing;
+
+/* Tests for behavior that happens before the server responds to FUSE_INIT */
+class PreInit: public FuseTest {
+void SetUp() {
+ m_no_auto_init = true;
+ FuseTest::SetUp();
+}
+};
+
+static void* unmount1(void* arg __unused) {
+ ssize_t r;
+
+ r = unmount("mountpoint", 0);
+ if (r >= 0)
+ return 0;
+ else
+ return (void*)(intptr_t)errno;
+}
+
+/*
+ * Attempting to unmount the file system before it fully initializes should
+ * work fine. The unmount will complete after initialization does.
+ */
+TEST_F(PreInit, unmount_before_init)
+{
+ sem_t sem0;
+ pthread_t th1;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_INIT);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, init);
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ out.body.init.minor = FUSE_KERNEL_MINOR_VERSION;
+ out.body.init.flags = in.body.init.flags & m_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = m_maxreadahead;
+ out.body.init.time_gran = m_time_gran;
+ sem_wait(&sem0);
+ })));
+ expect_destroy(0);
+
+ ASSERT_EQ(0, pthread_create(&th1, NULL, unmount1, NULL));
+ nap(); /* Wait for th1 to block in unmount() */
+ sem_post(&sem0);
+ /* The daemon will quit after receiving FUSE_DESTROY */
+ m_mock->join_daemon();
+}
+
+/*
+ * Don't panic in this very specific scenario:
+ *
+ * The server does not respond to FUSE_INIT in timely fashion.
+ * Some other process tries to do unmount.
+ * That other process gets killed by a signal.
+ * The server finally responds to FUSE_INIT.
+ *
+ * Regression test for bug 287438
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=287438
+ */
+TEST_F(PreInit, signal_during_unmount_before_init)
+{
+ sem_t sem0;
+ pid_t child;
+
+ ASSERT_EQ(0, sem_init(&sem0, 0, 0)) << strerror(errno);
+
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([](auto in) {
+ return (in.header.opcode == FUSE_INIT);
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(ReturnImmediate([&](auto in, auto& out) {
+ SET_OUT_HEADER_LEN(out, init);
+ out.body.init.major = FUSE_KERNEL_VERSION;
+ /*
+ * Use protocol 7.19, like libfuse2 does. The server must use
+ * protocol 7.27 or older to trigger the bug.
+ */
+ out.body.init.minor = 19;
+ out.body.init.flags = in.body.init.flags & m_init_flags;
+ out.body.init.max_write = m_maxwrite;
+ out.body.init.max_readahead = m_maxreadahead;
+ out.body.init.time_gran = m_time_gran;
+ sem_wait(&sem0);
+ })));
+
+ if ((child = ::fork()) == 0) {
+ /*
+ * In child. This will block waiting for FUSE_INIT to complete
+ * or the receipt of an asynchronous signal.
+ */
+ (void) unmount("mountpoint", 0);
+ _exit(0); /* Unreachable, unless parent dies after fork */
+ } else if (child > 0) {
+ /* In parent. Wait for child process to start, then kill it */
+ nap();
+ kill(child, SIGINT);
+ waitpid(child, NULL, WEXITED);
+ } else {
+ FAIL() << strerror(errno);
+ }
+ m_mock->m_quit = true; /* Since we are by now unmounted. */
+ sem_post(&sem0);
+ m_mock->join_daemon();
+}
diff --git a/tests/sys/fs/fusefs/read.cc b/tests/sys/fs/fusefs/read.cc
index 373f742d4fd3..e9c79ba2ffda 100644
--- a/tests/sys/fs/fusefs/read.cc
+++ b/tests/sys/fs/fusefs/read.cc
@@ -111,6 +111,13 @@ class ReadAhead: public Read,
}
};
+class ReadMaxRead: public Read {
+ virtual void SetUp() {
+ m_maxread = 16384;
+ Read::SetUp();
+ }
+};
+
class ReadNoatime: public Read {
virtual void SetUp() {
m_noatime = true;
@@ -840,6 +847,52 @@ TEST_F(Read, mmap)
leak(fd);
}
+
+/* When max_read is set, large reads will be split up as necessary */
+TEST_F(ReadMaxRead, split)
+{
+ const char FULLPATH[] = "mountpoint/some_file.txt";
+ const char RELPATH[] = "some_file.txt";
+ uint64_t ino = 42;
+ int fd;
+ ssize_t bufsize = 65536;
+ ssize_t fragsize = bufsize / 4;
+ char *rbuf, *frag0, *frag1, *frag2, *frag3;
+
+ rbuf = new char[bufsize]();
+ frag0 = new char[fragsize]();
+ frag1 = new char[fragsize]();
+ frag2 = new char[fragsize]();
+ frag3 = new char[fragsize]();
+ memset(frag0, '0', fragsize);
+ memset(frag1, '1', fragsize);
+ memset(frag2, '2', fragsize);
+ memset(frag3, '3', fragsize);
+
+ expect_lookup(RELPATH, ino, bufsize);
+ expect_open(ino, 0, 1);
+ expect_read(ino, 0, fragsize, fragsize, frag0);
+ expect_read(ino, fragsize, fragsize, fragsize, frag1);
+ expect_read(ino, 2 * fragsize, fragsize, fragsize, frag2);
+ expect_read(ino, 3 * fragsize, fragsize, fragsize, frag3);
+
+ fd = open(FULLPATH, O_RDONLY);
+ ASSERT_LE(0, fd) << strerror(errno);
+
+ ASSERT_EQ(bufsize, read(fd, rbuf, bufsize)) << strerror(errno);
+ ASSERT_EQ(0, memcmp(rbuf, frag0, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + fragsize, frag1, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + 2 * fragsize, frag2, fragsize));
+ ASSERT_EQ(0, memcmp(rbuf + 3 * fragsize, frag3, fragsize));
+
+ delete[] frag3;
+ delete[] frag2;
+ delete[] frag1;
+ delete[] frag0;
+ delete[] rbuf;
+ leak(fd);
+}
+
/*
* The kernel should not update the cached atime attribute during a read, if
* MNT_NOATIME is used.
@@ -1339,7 +1392,7 @@ TEST_P(ReadAhead, readahead) {
expect_open(ino, 0, 1);
maxcontig = m_noclusterr ? m_maxbcachebuf :
m_maxbcachebuf + m_maxreadahead;
- clustersize = MIN(maxcontig, m_maxphys);
+ clustersize = MIN((unsigned long )maxcontig, m_maxphys);
for (offs = 0; offs < bufsize; offs += clustersize) {
len = std::min((size_t)clustersize, (size_t)(filesize - offs));
expect_read(ino, offs, len, len, contents + offs);
diff --git a/tests/sys/fs/fusefs/unlink.cc b/tests/sys/fs/fusefs/unlink.cc
index db54e286d85d..1d8a371649ee 100644
--- a/tests/sys/fs/fusefs/unlink.cc
+++ b/tests/sys/fs/fusefs/unlink.cc
@@ -1,6 +1,5 @@
/*-
* Copyright (c) 2019 The FreeBSD Foundation
- * All rights reserved.
*
* This software was developed by BFF Storage Systems, LLC under sponsorship
* from the FreeBSD Foundation.
diff --git a/tests/sys/fs/fusefs/utils.cc b/tests/sys/fs/fusefs/utils.cc
index 55a552e28eeb..125b7e2d6fc7 100644
--- a/tests/sys/fs/fusefs/utils.cc
+++ b/tests/sys/fs/fusefs/utils.cc
@@ -124,29 +124,21 @@ bool is_unsafe_aio_enabled(void) {
class FuseEnv: public Environment {
virtual void SetUp() {
+ check_environment();
}
};
void FuseTest::SetUp() {
const char *maxbcachebuf_node = "vfs.maxbcachebuf";
const char *maxphys_node = "kern.maxphys";
- int val = 0;
- size_t size = sizeof(val);
-
- /*
- * XXX check_environment should be called from FuseEnv::SetUp, but
- * can't due to https://github.com/google/googletest/issues/2189
- */
- check_environment();
- if (IsSkipped())
- return;
+ size_t size;
- ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &val, &size, NULL, 0))
- << strerror(errno);
- m_maxbcachebuf = val;
- ASSERT_EQ(0, sysctlbyname(maxphys_node, &val, &size, NULL, 0))
+ size = sizeof(m_maxbcachebuf);
+ ASSERT_EQ(0, sysctlbyname(maxbcachebuf_node, &m_maxbcachebuf, &size,
+ NULL, 0)) << strerror(errno);
+ size = sizeof(m_maxphys);
+ ASSERT_EQ(0, sysctlbyname(maxphys_node, &m_maxphys, &size, NULL, 0))
<< strerror(errno);
- m_maxphys = val;
/*
* Set the default max_write to a distinct value from MAXPHYS to catch
* bugs that confuse the two.
@@ -155,11 +147,12 @@ void FuseTest::SetUp() {
m_maxwrite = MIN(libfuse_max_write, (uint32_t)m_maxphys / 2);
try {
- m_mock = new MockFS(m_maxreadahead, m_allow_other,
+ m_mock = new MockFS(m_maxread, m_maxreadahead, m_allow_other,
m_default_permissions, m_push_symlinks_in, m_ro,
m_pm, m_init_flags, m_kernel_minor_version,
m_maxwrite, m_async, m_noclusterr, m_time_gran,
- m_nointr, m_noatime, m_fsname, m_subtype);
+ m_nointr, m_noatime, m_fsname, m_subtype,
+ m_no_auto_init);
/*
* FUSE_ACCESS is called almost universally. Expecting it in
* each test case would be super-annoying. Instead, set a
diff --git a/tests/sys/fs/fusefs/utils.hh b/tests/sys/fs/fusefs/utils.hh
index 383f01ea4bfe..91bbba909672 100644
--- a/tests/sys/fs/fusefs/utils.hh
+++ b/tests/sys/fs/fusefs/utils.hh
@@ -55,6 +55,7 @@ bool is_unsafe_aio_enabled(void);
extern const uint32_t libfuse_max_write;
class FuseTest : public ::testing::Test {
protected:
+ uint32_t m_maxread;
uint32_t m_maxreadahead;
uint32_t m_maxwrite;
uint32_t m_init_flags;
@@ -68,6 +69,7 @@ class FuseTest : public ::testing::Test {
bool m_async;
bool m_noclusterr;
bool m_nointr;
+ bool m_no_auto_init;
unsigned m_time_gran;
MockFS *m_mock = NULL;
const static uint64_t FH = 0xdeadbeef1a7ebabe;
@@ -77,9 +79,10 @@ class FuseTest : public ::testing::Test {
public:
int m_maxbcachebuf;
- int m_maxphys;
+ unsigned long m_maxphys;
FuseTest():
+ m_maxread(0),
m_maxreadahead(0),
m_maxwrite(0),
m_init_flags(0),
@@ -93,6 +96,7 @@ class FuseTest : public ::testing::Test {
m_async(false),
m_noclusterr(false),
m_nointr(false),
+ m_no_auto_init(false),
m_time_gran(1),
m_fsname(""),
m_subtype(""),
diff --git a/tests/sys/fs/fusefs/write.cc b/tests/sys/fs/fusefs/write.cc
index f931f350a7c3..1fe2e3cc522d 100644
--- a/tests/sys/fs/fusefs/write.cc
+++ b/tests/sys/fs/fusefs/write.cc
@@ -179,12 +179,12 @@ class WriteCluster: public WriteBack {
public:
virtual void SetUp() {
m_async = true;
- m_maxwrite = 1 << 25; // Anything larger than MAXPHYS will suffice
+ m_maxwrite = UINT32_MAX; // Anything larger than MAXPHYS will suffice
WriteBack::SetUp();
if (m_maxphys < 2 * DFLTPHYS)
GTEST_SKIP() << "MAXPHYS must be at least twice DFLTPHYS"
<< " for this test";
- if (m_maxphys < 2 * m_maxbcachebuf)
+ if (m_maxphys < 2 * (unsigned long )m_maxbcachebuf)
GTEST_SKIP() << "MAXPHYS must be at least twice maxbcachebuf"
<< " for this test";
}
@@ -860,7 +860,8 @@ TEST_F(WriteMaxWrite, write)
ssize_t halfbufsize, bufsize;
halfbufsize = m_mock->m_maxwrite;
- if (halfbufsize >= m_maxbcachebuf || halfbufsize >= m_maxphys)
+ if (halfbufsize >= m_maxbcachebuf ||
+ (unsigned long )halfbufsize >= m_maxphys)
GTEST_SKIP() << "Must lower m_maxwrite for this test";
bufsize = halfbufsize * 2;
contents = new int[bufsize / sizeof(int)];
diff --git a/tests/sys/fs/fusefs/xattr.cc b/tests/sys/fs/fusefs/xattr.cc
index 7fa2a741e074..0ab203c96254 100644
--- a/tests/sys/fs/fusefs/xattr.cc
+++ b/tests/sys/fs/fusefs/xattr.cc
@@ -110,6 +110,8 @@ void expect_setxattr(uint64_t ino, const char *attr, const char *value,
const char *v = a + strlen(a) + 1;
return (in.header.opcode == FUSE_SETXATTR &&
in.header.nodeid == ino &&
+ in.body.setxattr.size == (strlen(value) + 1) &&
+ in.body.setxattr.setxattr_flags == 0 &&
0 == strcmp(attr, a) &&
0 == strcmp(value, v));
}, Eq(true)),
@@ -119,6 +121,33 @@ void expect_setxattr(uint64_t ino, const char *attr, const char *value,
};
+class Xattr_7_32:public FuseTest {
+public:
+virtual void SetUp()
+{
+ m_kernel_minor_version = 32;
+ FuseTest::SetUp();
+}
+
+void expect_setxattr_7_32(uint64_t ino, const char *attr, const char *value,
+ ProcessMockerT r)
+{
+ EXPECT_CALL(*m_mock, process(
+ ResultOf([=](auto in) {
+ const char *a = (const char *)in.body.bytes +
+ FUSE_COMPAT_SETXATTR_IN_SIZE;
+ const char *v = a + strlen(a) + 1;
+ return (in.header.opcode == FUSE_SETXATTR &&
+ in.header.nodeid == ino &&
+ in.body.setxattr.size == (strlen(value) + 1) &&
+ 0 == strcmp(attr, a) &&
+ 0 == strcmp(value, v));
+ }, Eq(true)),
+ _)
+ ).WillOnce(Invoke(r));
+}
+};
+
class Getxattr: public Xattr {};
class Listxattr: public Xattr {};
@@ -153,6 +182,7 @@ void TearDown() {
class Removexattr: public Xattr {};
class Setxattr: public Xattr {};
+class Setxattr_7_32:public Xattr_7_32 {};
class RofsXattr: public Xattr {
public:
virtual void SetUp() {
@@ -350,7 +380,7 @@ TEST_F(Listxattr, enotsup)
* On Linux, however, the file system is supposed to return ERANGE if an
* insufficiently large buffer is passed to listxattr(2).
*
- * fusefs(5) must guarantee the usual FreeBSD behavior.
+ * fusefs(4) must guarantee the usual FreeBSD behavior.
*/
TEST_F(Listxattr, erange)
{
@@ -569,7 +599,7 @@ TEST_F(Listxattr, size_only_race_smaller)
}));
expect_listxattr(ino, sizeof(attrs0),
ReturnImmediate([&](auto in __unused, auto& out) {
- strlcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
+ memcpy((char*)out.body.bytes, attrs1, sizeof(attrs1));
out.header.len = sizeof(fuse_out_header) +
sizeof(attrs1);
})
@@ -728,6 +758,7 @@ TEST_F(Removexattr, system)
<< strerror(errno);
}
+
/*
* If the filesystem returns ENOSYS, then it will be treated as a permanent
* failure and all future VOP_SETEXTATTR calls will fail with EOPNOTSUPP
@@ -815,6 +846,23 @@ TEST_F(Setxattr, system)
ASSERT_EQ(value_len, r) << strerror(errno);
}
+
+TEST_F(Setxattr_7_32, ok)
+{
+ uint64_t ino = 42;
+ const char value[] = "whatever";
+ ssize_t value_len = strlen(value) + 1;
+ int ns = EXTATTR_NAMESPACE_USER;
+ ssize_t r;
+
+ expect_lookup(RELPATH, ino, S_IFREG | 0644, 0, 1);
+ expect_setxattr_7_32(ino, "user.foo", value, ReturnErrno(0));
+
+ r = extattr_set_file(FULLPATH, ns, "foo", (const void *)value,
+ value_len);
+ ASSERT_EQ(value_len, r) << strerror(errno);
+}
+
TEST_F(RofsXattr, deleteextattr_erofs)
{
uint64_t ino = 42;
diff --git a/tests/sys/fs/tarfs/tarfs_test.sh b/tests/sys/fs/tarfs/tarfs_test.sh
index f1322033fbad..20baadfea5c5 100644
--- a/tests/sys/fs/tarfs/tarfs_test.sh
+++ b/tests/sys/fs/tarfs/tarfs_test.sh
@@ -48,7 +48,6 @@ tarsum() {
}
tarfs_setup() {
- kldload -n tarfs || atf_skip "This test requires tarfs and could not load it"
mkdir "${mnt}"
}
@@ -60,6 +59,7 @@ atf_test_case tarfs_basic cleanup
tarfs_basic_head() {
atf_set "descr" "Basic function test"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_basic_body() {
tarfs_setup
@@ -87,6 +87,7 @@ atf_test_case tarfs_basic_gnu cleanup
tarfs_basic_gnu_head() {
atf_set "descr" "Basic function test using GNU tar"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_basic_gnu_body() {
@@ -101,6 +102,7 @@ atf_test_case tarfs_notdir_device cleanup
tarfs_notdir_device_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_notdir_device_body() {
tarfs_setup
@@ -121,6 +123,7 @@ atf_test_case tarfs_notdir_device_gnu cleanup
tarfs_notdir_device_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_device_gnu_body() {
@@ -135,6 +138,7 @@ atf_test_case tarfs_notdir_dot cleanup
tarfs_notdir_dot_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_notdir_dot_body() {
tarfs_setup
@@ -155,6 +159,7 @@ atf_test_case tarfs_notdir_dot_gnu cleanup
tarfs_notdir_dot_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_dot_gnu_body() {
@@ -169,6 +174,7 @@ atf_test_case tarfs_notdir_dotdot cleanup
tarfs_notdir_dotdot_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_notdir_dotdot_body() {
tarfs_setup
@@ -189,6 +195,7 @@ atf_test_case tarfs_notdir_dotdot_gnu cleanup
tarfs_notdir_dotdot_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_dotdot_gnu_body() {
@@ -203,6 +210,7 @@ atf_test_case tarfs_notdir_file cleanup
tarfs_notdir_file_head() {
atf_set "descr" "Regression test for PR 269519 and 269561"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_notdir_file_body() {
tarfs_setup
@@ -223,6 +231,7 @@ atf_test_case tarfs_notdir_file_gnu cleanup
tarfs_notdir_file_gnu_head() {
atf_set "descr" "Regression test for PR 269519 and 269561 using GNU tar"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "gtar"
}
tarfs_notdir_file_gnu_body() {
@@ -237,6 +246,7 @@ atf_test_case tarfs_emptylink cleanup
tarfs_emptylink_head() {
atf_set "descr" "Regression test for PR 277360: empty link target"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_emptylink_body() {
tarfs_setup
@@ -256,6 +266,7 @@ atf_test_case tarfs_linktodir cleanup
tarfs_linktodir_head() {
atf_set "descr" "Regression test for PR 277360: link to directory"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_linktodir_body() {
tarfs_setup
@@ -276,6 +287,7 @@ atf_test_case tarfs_linktononexistent cleanup
tarfs_linktononexistent_head() {
atf_set "descr" "Regression test for PR 277360: link to nonexistent target"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_linktononexistent_body() {
tarfs_setup
@@ -293,6 +305,7 @@ atf_test_case tarfs_checksum cleanup
tarfs_checksum_head() {
atf_set "descr" "Verify that the checksum covers header padding"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_checksum_body() {
tarfs_setup
@@ -313,6 +326,7 @@ atf_test_case tarfs_long_names cleanup
tarfs_long_names_head() {
atf_set "descr" "Verify that tarfs supports long file names"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_long_names_body() {
tarfs_setup
@@ -337,6 +351,7 @@ atf_test_case tarfs_long_paths cleanup
tarfs_long_paths_head() {
atf_set "descr" "Verify that tarfs supports long paths"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
}
tarfs_long_paths_body() {
tarfs_setup
@@ -361,6 +376,7 @@ atf_test_case tarfs_git_archive cleanup
tarfs_git_archive_head() {
atf_set "descr" "Verify that tarfs supports archives created by git"
atf_set "require.user" "root"
+ atf_set "require.kmods" "tarfs"
atf_set "require.progs" "git"
}
tarfs_git_archive_body() {
diff --git a/tests/sys/fs/tmpfs/Makefile b/tests/sys/fs/tmpfs/Makefile
index 106b28c7c463..ce4ca959838c 100644
--- a/tests/sys/fs/tmpfs/Makefile
+++ b/tests/sys/fs/tmpfs/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
FILESYSTEM?= ${.CURDIR:T}
diff --git a/tests/sys/geom/Makefile b/tests/sys/geom/Makefile
index 7e6e7cd2bb65..78257e180cf9 100644
--- a/tests/sys/geom/Makefile
+++ b/tests/sys/geom/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/geom
TESTS_SUBDIRS+= class
diff --git a/tests/sys/geom/class/Makefile b/tests/sys/geom/class/Makefile
index 10b01a043ddf..3cf3a15273ac 100644
--- a/tests/sys/geom/class/Makefile
+++ b/tests/sys/geom/class/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
@@ -19,6 +18,7 @@ TESTS_SUBDIRS+= shsec
TESTS_SUBDIRS+= stripe
TESTS_SUBDIRS+= union
TESTS_SUBDIRS+= uzip
+TESTS_SUBDIRS+= virstor
${PACKAGE}FILES+= geom_subr.sh
diff --git a/tests/sys/geom/class/Makefile.inc b/tests/sys/geom/class/Makefile.inc
index 03efce15f856..cec69b26e149 100644
--- a/tests/sys/geom/class/Makefile.inc
+++ b/tests/sys/geom/class/Makefile.inc
@@ -1,2 +1 @@
-
.include "${SRCTOP}/tests/Makefile.inc0"
diff --git a/tests/sys/geom/class/concat/Makefile b/tests/sys/geom/class/concat/Makefile
index 335c7632c6e5..d5457b64bd2a 100644
--- a/tests/sys/geom/class/concat/Makefile
+++ b/tests/sys/geom/class/concat/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/eli/Makefile b/tests/sys/geom/class/eli/Makefile
index d8faff182ac4..e1a28f39d5d2 100644
--- a/tests/sys/geom/class/eli/Makefile
+++ b/tests/sys/geom/class/eli/Makefile
@@ -1,4 +1,3 @@
-
.PATH: ${SRCTOP}/sys/geom/eli ${SRCTOP}/sys/crypto/sha2
PACKAGE= tests
diff --git a/tests/sys/geom/class/eli/attach_test.sh b/tests/sys/geom/class/eli/attach_test.sh
index eb59234f014b..b6b1848f2d37 100644
--- a/tests/sys/geom/class/eli/attach_test.sh
+++ b/tests/sys/geom/class/eli/attach_test.sh
@@ -39,6 +39,34 @@ attach_d_cleanup()
geli_test_cleanup
}
+atf_test_case atach_multiple_fails cleanup
+attach_multiple_fails_head()
+{
+ atf_set "descr" "test multiple failed attach of geli provider"
+ atf_set "require.user" "root"
+}
+attach_multiple_fails_body()
+{
+ geli_test_setup
+
+ sectors=1000
+ attach_md md -t malloc -s `expr $sectors + 1`
+ atf_check dd if=/dev/random of=keyfile bs=512 count=16 status=none
+
+ atf_check geli init -B none -P -K keyfile ${md}
+ atf_check geli attach -d -p -k keyfile ${md}
+
+ for i in $(jot 100); do
+ atf_check -s not-exit:0 -e ignore -- geli attach -d -p -k keyfile ${md}
+ done
+ atf_check -o ignore -- newfs ${md}.eli
+}
+attach_multiple_fails_cleanup()
+{
+ geli_test_cleanup
+}
+
+
atf_test_case attach_r cleanup
attach_r_head()
{
@@ -125,5 +153,6 @@ atf_init_test_cases()
atf_add_test_case attach_d
atf_add_test_case attach_r
atf_add_test_case attach_multiple
+ atf_add_test_case attach_multiple_fails
atf_add_test_case nokey
}
diff --git a/tests/sys/geom/class/gate/Makefile b/tests/sys/geom/class/gate/Makefile
index a51c983f5cf6..5ebc1d9304e0 100644
--- a/tests/sys/geom/class/gate/Makefile
+++ b/tests/sys/geom/class/gate/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/gate/ggate_test.sh b/tests/sys/geom/class/gate/ggate_test.sh
index 3ca5c3a2531a..4cbc5d80ae89 100644
--- a/tests/sys/geom/class/gate/ggate_test.sh
+++ b/tests/sys/geom/class/gate/ggate_test.sh
@@ -1,7 +1,5 @@
-
PIDFILE=ggated.pid
PLAINFILES=plainfiles
-PORT=33080
CONF=gg.exports
atf_test_case ggatec_trim cleanup
@@ -17,13 +15,14 @@ ggatec_trim_body()
{
load_ggate
+ port=33080
us=$(alloc_ggate_dev)
work=$(alloc_md)
atf_check -e ignore -o ignore dd if=/dev/random of=/dev/$work bs=1m count=1 conv=notrunc
echo $CONF >> $PLAINFILES
echo "localhost RW /dev/$work" > $CONF
- atf_check ggated -p $PORT -F $PIDFILE $CONF
- atf_check ggatec create -p $PORT -u $us localhost /dev/$work
+ atf_check ggated -p $port -F $PIDFILE $CONF
+ atf_check ggatec create -p $port -u $us localhost /dev/$work
ggate_dev=/dev/ggate${us}
wait_for_ggate_device ${ggate_dev}
@@ -55,6 +54,7 @@ ggated_body()
load_ggate
+ port=33081
us=$(alloc_ggate_dev)
work=$(alloc_md)
src=$(alloc_md)
@@ -67,8 +67,8 @@ ggated_body()
echo $CONF >> $PLAINFILES
echo "127.0.0.1 RW /dev/$work" > $CONF
- atf_check ggated -p $PORT -F $PIDFILE $CONF
- atf_check ggatec create -p $PORT -u $us 127.0.0.1 /dev/$work
+ atf_check ggated -p $port -F $PIDFILE $CONF
+ atf_check ggatec create -p $port -u $us 127.0.0.1 /dev/$work
ggate_dev=/dev/ggate${us}
diff --git a/tests/sys/geom/class/mirror/Makefile b/tests/sys/geom/class/mirror/Makefile
index fdef410ab974..635288cc3d53 100644
--- a/tests/sys/geom/class/mirror/Makefile
+++ b/tests/sys/geom/class/mirror/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/multipath/Makefile b/tests/sys/geom/class/multipath/Makefile
index 9a2a9a29ec38..1246015e8fc4 100644
--- a/tests/sys/geom/class/multipath/Makefile
+++ b/tests/sys/geom/class/multipath/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/nop/Makefile b/tests/sys/geom/class/nop/Makefile
index 54b41584d441..e2cb249bcf56 100644
--- a/tests/sys/geom/class/nop/Makefile
+++ b/tests/sys/geom/class/nop/Makefile
@@ -1,8 +1,11 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
ATF_TESTS_SH+= nop_test
+# Some tests make use of the "disks" property and kyua may schedule
+# them to run at the time time, which the tests do not expect.
+TEST_METADATA.nop_test+= is_exclusive="true"
+
.include <bsd.test.mk>
diff --git a/tests/sys/geom/class/part/Makefile b/tests/sys/geom/class/part/Makefile
index 6ca33655c805..0d7d86224437 100644
--- a/tests/sys/geom/class/part/Makefile
+++ b/tests/sys/geom/class/part/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/raid3/Makefile b/tests/sys/geom/class/raid3/Makefile
index 97b9cd34b8f0..8f398a0f5fec 100644
--- a/tests/sys/geom/class/raid3/Makefile
+++ b/tests/sys/geom/class/raid3/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/shsec/Makefile b/tests/sys/geom/class/shsec/Makefile
index 7615142f605f..500c14cca51a 100644
--- a/tests/sys/geom/class/shsec/Makefile
+++ b/tests/sys/geom/class/shsec/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/stripe/Makefile b/tests/sys/geom/class/stripe/Makefile
index 7615142f605f..500c14cca51a 100644
--- a/tests/sys/geom/class/stripe/Makefile
+++ b/tests/sys/geom/class/stripe/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
diff --git a/tests/sys/geom/class/virstor/Makefile b/tests/sys/geom/class/virstor/Makefile
new file mode 100644
index 000000000000..67242879e33f
--- /dev/null
+++ b/tests/sys/geom/class/virstor/Makefile
@@ -0,0 +1,9 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
+
+ATF_TESTS_SH+= virstor_test
+
+${PACKAGE}FILES+= conf.sh
+
+.include <bsd.test.mk>
diff --git a/tests/sys/geom/class/virstor/conf.sh b/tests/sys/geom/class/virstor/conf.sh
new file mode 100644
index 000000000000..46b0fd1308a3
--- /dev/null
+++ b/tests/sys/geom/class/virstor/conf.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+class="virstor"
+base=$(atf_get ident)
+TEST_VIRSTOR_DEVS_FILE="${TMPDIR}/test_virstor_devs.$(basename $0)"
+
+gvirstor_dev_setup()
+{
+ # Pick a random name and record it for cleanup.
+ local vdevbase="$(mktemp -u virstor.XXXXXX)" || aft_fail "mktemp"
+ echo "$vdevbase" >> "$TEST_VIRSTOR_DEVS_FILE"
+ eval "${1}='${vdevbase}'"
+}
+
+gvirstor_test_cleanup()
+{
+ local vdevbase
+ if [ -f "$TEST_VIRSTOR_DEVS_FILE" ]; then
+ while read vdevbase; do
+ if [ -c "/dev/$class/$vdevbase" ]; then
+ echo "# Destroying test virstor device:" \
+ "$vdevbase"
+ gvirstor destroy "$vdevbase"
+ fi
+ done < "$TEST_VIRSTOR_DEVS_FILE"
+ fi
+ geom_test_cleanup
+}
+
+ATF_TEST=true
+. `dirname $0`/../geom_subr.sh
diff --git a/tests/sys/geom/class/virstor/virstor_test.sh b/tests/sys/geom/class/virstor/virstor_test.sh
new file mode 100644
index 000000000000..4f2047bffe97
--- /dev/null
+++ b/tests/sys/geom/class/virstor/virstor_test.sh
@@ -0,0 +1,73 @@
+#
+# Copyright (c) 2024 Dell Inc. or its subsidiaries. All Rights Reserved.
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+. $(atf_get_srcdir)/conf.sh
+
+atf_test_case basic cleanup
+basic_head()
+{
+ atf_set "descr" "geom virstor basic functional test"
+ atf_set "require.user" "root"
+}
+basic_body()
+{
+ geom_atf_test_setup
+ # Choose a virstor device name
+ gvirstor_dev_setup name
+
+ # Create an md backing device and initialize it with junk
+ psecsize=512
+ attach_md md -t swap -S $psecsize -s 5M || atf_fail "attach_md"
+ jot -b uninitialized 0 | dd status=none of=/dev/$md 2> /dev/null
+
+ # Create a virstor device
+ vsizemb=64
+ vsize=$((vsizemb * 1024 * 1024))
+ atf_check -o ignore -e ignore \
+ gvirstor label -v -s ${vsizemb}M -m 512 $name /dev/$md
+ devwait
+ vdev="/dev/$class/$name"
+
+ ssize=$(diskinfo $vdev | awk '{print $2}')
+ atf_check_equal $psecsize $ssize
+
+ size=$(diskinfo $vdev | awk '{print $3}')
+ atf_check_equal $vsize $size
+
+ # Write the first and last sectors of the virtual address space
+ hasha=$(jot -b a 0 | head -c $ssize | sha1)
+ hashz=$(jot -b z 0 | head -c $ssize | sha1)
+ zsector=$((vsize / ssize - 1))
+ jot -b a 0 | dd status=none of=$vdev bs=$ssize count=1 conv=notrunc
+ jot -b z 0 | dd status=none of=$vdev bs=$ssize count=1 conv=notrunc \
+ seek=$zsector
+
+ # Read back and compare
+ hashx=$(dd status=none if=$vdev bs=$ssize count=1 | sha1)
+ atf_check_equal $hasha $hashx
+ hashx=$(dd status=none if=$vdev bs=$ssize count=1 skip=$zsector | sha1)
+ atf_check_equal $hashz $hashx
+
+ # Destroy, then retaste and reload
+ atf_check -o ignore gvirstor destroy $name
+ true > /dev/$md
+ devwait
+
+ # Read back and compare
+ hashx=$(dd status=none if=$vdev bs=$ssize count=1 | sha1)
+ atf_check_equal $hasha $hashx
+ hashx=$(dd status=none if=$vdev bs=$ssize count=1 skip=$zsector | sha1)
+ atf_check_equal $hashz $hashx
+}
+basic_cleanup()
+{
+ gvirstor_test_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case basic
+}
diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index c5107ce8f9fe..26c0013696c7 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
PACKAGE= tests
@@ -16,7 +15,11 @@ ATF_TESTS_C+= kcov
.endif
ATF_TESTS_C+= kern_copyin
ATF_TESTS_C+= kern_descrip_test
+# One test modifies the maxfiles limit, which can cause spurious test failures.
+TEST_METADATA.kern_descrip_test+= is_exclusive="true"
ATF_TESTS_C+= fdgrowtable_test
+ATF_TESTS_C+= jail_lookup_root
+ATF_TESTS_C+= inotify_test
ATF_TESTS_C+= kill_zombie
.if ${MK_OPENSSL} != "no"
ATF_TESTS_C+= ktls_test
@@ -24,6 +27,7 @@ ATF_TESTS_C+= ktls_test
ATF_TESTS_C+= ktrace_test
ATF_TESTS_C+= listener_wakeup
ATF_TESTS_C+= module_test
+ATF_TESTS_C+= prace
ATF_TESTS_C+= ptrace_test
TEST_METADATA.ptrace_test+= timeout="15"
ATF_TESTS_C+= reaper
@@ -35,6 +39,7 @@ ATF_TESTS_C+= socket_accept
ATF_TESTS_C+= socket_accf
ATF_TESTS_C+= socket_msg_trunc
ATF_TESTS_C+= socket_msg_waitall
+ATF_TESTS_C+= socket_splice
TEST_METADATA.sigwait+= is_exclusive="true"
.if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH:Mpowerpc*} == ""
ATF_TESTS_C+= subr_physmem_test
@@ -57,12 +62,14 @@ ATF_TESTS_C+= sigsys
TEST_METADATA.sigsys+= is_exclusive="true"
ATF_TESTS_SH+= coredump_phnum_test
+ATF_TESTS_SH+= logsigexit_test
+ATF_TESTS_SH+= jailmeta
ATF_TESTS_SH+= sonewconn_overflow
TEST_METADATA.sonewconn_overflow+= required_programs="python"
TEST_METADATA.sonewconn_overflow+= required_user="root"
+TEST_METADATA.sonewconn_overflow+= is_exclusive="true"
ATF_TESTS_SH+= sendfile_test
ATF_TESTS_SH+= sysctl_security_jail_children
-TEST_METADATA.sysctl_security_jail_children+= is_exclusive="true"
${PACKAGE}FILES+= sonewconn_overflow.py
${PACKAGE}FILESMODE_sonewconn_overflow.py=0555
@@ -72,22 +79,28 @@ PROGS+= coredump_phnum_helper
PROGS+= pdeathsig_helper
PROGS+= sendfile_helper
+LIBADD.jail_lookup_root+= jail util
CFLAGS.sys_getrandom+= -I${SRCTOP}/sys/contrib/zstd/lib
LIBADD.sys_getrandom+= zstd
LIBADD.sys_getrandom+= c
LIBADD.sys_getrandom+= pthread
LIBADD.ptrace_test+= pthread
LIBADD.unix_seqpacket_test+= pthread
+LIBADD.inotify_test+= util
LIBADD.kcov+= pthread
CFLAGS.ktls_test+= -DOPENSSL_API_COMPAT=0x10100000L
LIBADD.ktls_test+= crypto util
LIBADD.listener_wakeup+= pthread
LIBADD.shutdown_dgram+= pthread
LIBADD.socket_msg_waitall+= pthread
+LIBADD.socket_splice+= pthread
LIBADD.sendfile_helper+= pthread
LIBADD.fdgrowtable_test+= util pthread kvm procstat
LIBADD.sigwait+= rt
LIBADD.ktrace_test+= sysdecode
+LIBADD.unix_passfd_dgram+= jail
+LIBADD.unix_passfd_stream+= jail
+LIBADD.unix_stream+= pthread
NETBSD_ATF_TESTS_C+= lockf_test
NETBSD_ATF_TESTS_C+= mqueue_test
@@ -125,6 +138,7 @@ WARNS?= 3
TESTS_SUBDIRS+= acct
TESTS_SUBDIRS+= execve
TESTS_SUBDIRS+= pipe
+TESTS_SUBDIRS+= tty
.include <netbsd-tests.test.mk>
diff --git a/tests/sys/kern/Makefile.inc b/tests/sys/kern/Makefile.inc
index d3b5cbd3a79b..01b5f23410c8 100644
--- a/tests/sys/kern/Makefile.inc
+++ b/tests/sys/kern/Makefile.inc
@@ -1,2 +1 @@
-
.include "../Makefile.inc"
diff --git a/tests/sys/kern/acct/Makefile b/tests/sys/kern/acct/Makefile
index 6f1181fe6d8a..779be0ae591d 100644
--- a/tests/sys/kern/acct/Makefile
+++ b/tests/sys/kern/acct/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/kern/acct
ATF_TESTS_C= acct_test
diff --git a/tests/sys/kern/coredump_phnum_test.sh b/tests/sys/kern/coredump_phnum_test.sh
index e01c5d86a252..a1337d5ad1fb 100644
--- a/tests/sys/kern/coredump_phnum_test.sh
+++ b/tests/sys/kern/coredump_phnum_test.sh
@@ -80,6 +80,11 @@ coredump_phnum_body()
atf_fail "Helper program did not dump core"
fi
+ if readelf --version | grep -q LLVM; then
+ atf_expect_fail "PR285547: llvm-objdump does not support large phdr count"
+ # See https://github.com/llvm/llvm-project/issues/132216
+ fi
+
# These magic numbers don't have any real significance. They are just
# the result of running the helper program and dumping core. The only
# important bit is that they're larger than 65535 (UINT16_MAX).
diff --git a/tests/sys/kern/execve/Makefile b/tests/sys/kern/execve/Makefile
index 77221b2d2dfa..afa537031c39 100644
--- a/tests/sys/kern/execve/Makefile
+++ b/tests/sys/kern/execve/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/kern/execve
BINDIR= ${TESTSDIR}
diff --git a/tests/sys/kern/inotify_test.c b/tests/sys/kern/inotify_test.c
new file mode 100644
index 000000000000..ed7cef5d148c
--- /dev/null
+++ b/tests/sys/kern/inotify_test.c
@@ -0,0 +1,862 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Klara, Inc.
+ */
+
+#include <sys/capsicum.h>
+#include <sys/filio.h>
+#include <sys/inotify.h>
+#include <sys/ioccom.h>
+#include <sys/mount.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysctl.h>
+#include <sys/un.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <mntopts.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static const char *
+ev2name(int event)
+{
+ switch (event) {
+ case IN_ACCESS:
+ return ("IN_ACCESS");
+ case IN_ATTRIB:
+ return ("IN_ATTRIB");
+ case IN_CLOSE_WRITE:
+ return ("IN_CLOSE_WRITE");
+ case IN_CLOSE_NOWRITE:
+ return ("IN_CLOSE_NOWRITE");
+ case IN_CREATE:
+ return ("IN_CREATE");
+ case IN_DELETE:
+ return ("IN_DELETE");
+ case IN_DELETE_SELF:
+ return ("IN_DELETE_SELF");
+ case IN_MODIFY:
+ return ("IN_MODIFY");
+ case IN_MOVE_SELF:
+ return ("IN_MOVE_SELF");
+ case IN_MOVED_FROM:
+ return ("IN_MOVED_FROM");
+ case IN_MOVED_TO:
+ return ("IN_MOVED_TO");
+ case IN_OPEN:
+ return ("IN_OPEN");
+ default:
+ return (NULL);
+ }
+}
+
+static void
+close_checked(int fd)
+{
+ ATF_REQUIRE(close(fd) == 0);
+}
+
+/*
+ * Make sure that no other events are pending, and close the inotify descriptor.
+ */
+static void
+close_inotify(int fd)
+{
+ int n;
+
+ ATF_REQUIRE(ioctl(fd, FIONREAD, &n) == 0);
+ ATF_REQUIRE(n == 0);
+ close_checked(fd);
+}
+
+static uint32_t
+consume_event_cookie(int ifd, int wd, int event, int flags, const char *name)
+{
+ struct inotify_event *ev;
+ size_t evsz, namelen;
+ ssize_t n;
+ uint32_t cookie;
+
+ /* Only read one record. */
+ namelen = name == NULL ? 0 : strlen(name);
+ evsz = sizeof(*ev) + _IN_NAMESIZE(namelen);
+ ev = malloc(evsz);
+ ATF_REQUIRE(ev != NULL);
+
+ n = read(ifd, ev, evsz);
+ ATF_REQUIRE_MSG(n >= 0, "failed to read event %s", ev2name(event));
+ ATF_REQUIRE((size_t)n >= sizeof(*ev));
+ ATF_REQUIRE((size_t)n == sizeof(*ev) + ev->len);
+ ATF_REQUIRE((size_t)n == evsz);
+
+ ATF_REQUIRE_MSG((ev->mask & IN_ALL_EVENTS) == event,
+ "expected event %#x, got %#x", event, ev->mask);
+ ATF_REQUIRE_MSG((ev->mask & _IN_ALL_RETFLAGS) == flags,
+ "expected flags %#x, got %#x", flags, ev->mask);
+ ATF_REQUIRE_MSG(ev->wd == wd,
+ "expected wd %d, got %d", wd, ev->wd);
+ ATF_REQUIRE_MSG(name == NULL || strcmp(name, ev->name) == 0,
+ "expected name '%s', got '%s'", name, ev->name);
+ cookie = ev->cookie;
+ if ((ev->mask & (IN_MOVED_FROM | IN_MOVED_TO)) == 0)
+ ATF_REQUIRE(cookie == 0);
+ free(ev);
+ return (cookie);
+}
+
+/*
+ * Read an event from the inotify file descriptor and check that it
+ * matches the expected values.
+ */
+static void
+consume_event(int ifd, int wd, int event, int flags, const char *name)
+{
+ (void)consume_event_cookie(ifd, wd, event, flags, name);
+}
+
+static int
+inotify(int flags)
+{
+ int ifd;
+
+ ifd = inotify_init1(flags);
+ ATF_REQUIRE(ifd != -1);
+ return (ifd);
+}
+
+static void
+mount_nullfs(char *dir, char *src)
+{
+ struct iovec *iov;
+ char errmsg[1024];
+ int error, iovlen;
+
+ iov = NULL;
+ iovlen = 0;
+
+ build_iovec(&iov, &iovlen, "fstype", "nullfs", (size_t)-1);
+ build_iovec(&iov, &iovlen, "fspath", dir, (size_t)-1);
+ build_iovec(&iov, &iovlen, "target", src, (size_t)-1);
+ build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg));
+
+ errmsg[0] = '\0';
+ error = nmount(iov, iovlen, 0);
+ ATF_REQUIRE_MSG(error == 0,
+ "mount nullfs %s %s: %s", src, dir,
+ errmsg[0] == '\0' ? strerror(errno) : errmsg);
+
+ free_iovec(&iov, &iovlen);
+}
+
+static void
+mount_tmpfs(const char *dir)
+{
+ struct iovec *iov;
+ char errmsg[1024];
+ int error, iovlen;
+
+ iov = NULL;
+ iovlen = 0;
+
+ build_iovec(&iov, &iovlen, "fstype", "tmpfs", (size_t)-1);
+ build_iovec(&iov, &iovlen, "fspath", __DECONST(char *, dir),
+ (size_t)-1);
+ build_iovec(&iov, &iovlen, "errmsg", errmsg, sizeof(errmsg));
+
+ errmsg[0] = '\0';
+ error = nmount(iov, iovlen, 0);
+ ATF_REQUIRE_MSG(error == 0,
+ "mount tmpfs %s: %s", dir,
+ errmsg[0] == '\0' ? strerror(errno) : errmsg);
+
+ free_iovec(&iov, &iovlen);
+}
+
+static int
+watch_file(int ifd, int events, char *path)
+{
+ int fd, wd;
+
+ strncpy(path, "test.XXXXXX", PATH_MAX);
+ fd = mkstemp(path);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+
+ wd = inotify_add_watch(ifd, path, events);
+ ATF_REQUIRE(wd != -1);
+
+ return (wd);
+}
+
+static int
+watch_dir(int ifd, int events, char *path)
+{
+ char *p;
+ int wd;
+
+ strlcpy(path, "test.XXXXXX", PATH_MAX);
+ p = mkdtemp(path);
+ ATF_REQUIRE(p == path);
+
+ wd = inotify_add_watch(ifd, path, events);
+ ATF_REQUIRE(wd != -1);
+
+ return (wd);
+}
+
+/*
+ * Verify that Capsicum restrictions are applied as expected.
+ */
+ATF_TC_WITHOUT_HEAD(inotify_capsicum);
+ATF_TC_BODY(inotify_capsicum, tc)
+{
+ int error, dfd, ifd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+ ATF_REQUIRE(ifd != -1);
+
+ dfd = open(".", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(dfd != -1);
+
+ error = mkdirat(dfd, "testdir", 0755);
+ ATF_REQUIRE(error == 0);
+
+ error = cap_enter();
+ ATF_REQUIRE(error == 0);
+
+ /*
+ * Plain inotify_add_watch() is disallowed.
+ */
+ wd = inotify_add_watch(ifd, ".", IN_DELETE_SELF);
+ ATF_REQUIRE_ERRNO(ECAPMODE, wd == -1);
+ wd = inotify_add_watch_at(ifd, dfd, "testdir", IN_DELETE_SELF);
+ ATF_REQUIRE(wd >= 0);
+
+ /*
+ * Generate a record and consume it.
+ */
+ error = unlinkat(dfd, "testdir", AT_REMOVEDIR);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL);
+ consume_event(ifd, wd, 0, IN_IGNORED, NULL);
+
+ close_checked(dfd);
+ close_inotify(ifd);
+}
+
+/*
+ * Make sure that duplicate, back-to-back events are coalesced.
+ */
+ATF_TC_WITHOUT_HEAD(inotify_coalesce);
+ATF_TC_BODY(inotify_coalesce, tc)
+{
+ char file[PATH_MAX], path[PATH_MAX];
+ int fd, fd1, ifd, n, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ /* Create a directory and watch it. */
+ wd = watch_dir(ifd, IN_OPEN, path);
+ /* Create a file in the directory and open it. */
+ snprintf(file, sizeof(file), "%s/file", path);
+ fd = open(file, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ fd = open(file, O_RDWR);
+ ATF_REQUIRE(fd != -1);
+ fd1 = open(file, O_RDONLY);
+ ATF_REQUIRE(fd1 != -1);
+ close_checked(fd1);
+ close_checked(fd);
+
+ consume_event(ifd, wd, IN_OPEN, 0, "file");
+ ATF_REQUIRE(ioctl(ifd, FIONREAD, &n) == 0);
+ ATF_REQUIRE(n == 0);
+
+ close_inotify(ifd);
+}
+
+/*
+ * Check handling of IN_MASK_CREATE.
+ */
+ATF_TC_WITHOUT_HEAD(inotify_mask_create);
+ATF_TC_BODY(inotify_mask_create, tc)
+{
+ char path[PATH_MAX];
+ int ifd, wd, wd1;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ /* Create a directory and watch it. */
+ wd = watch_dir(ifd, IN_CREATE, path);
+ /* Updating the watch with IN_MASK_CREATE should result in an error. */
+ wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_CREATE);
+ ATF_REQUIRE_ERRNO(EEXIST, wd1 == -1);
+ /* It's an error to specify IN_MASK_ADD with IN_MASK_CREATE. */
+ wd1 = inotify_add_watch(ifd, path, IN_MODIFY | IN_MASK_ADD |
+ IN_MASK_CREATE);
+ ATF_REQUIRE_ERRNO(EINVAL, wd1 == -1);
+ /* Updating the watch without IN_MASK_CREATE should work. */
+ wd1 = inotify_add_watch(ifd, path, IN_MODIFY);
+ ATF_REQUIRE(wd1 != -1);
+ ATF_REQUIRE_EQ(wd, wd1);
+
+ close_inotify(ifd);
+}
+
+/*
+ * Make sure that inotify cooperates with nullfs: if a lower vnode is the
+ * subject of an event, the upper vnode should be notified, and if the upper
+ * vnode is the subject of an event, the lower vnode should be notified.
+ */
+ATF_TC_WITH_CLEANUP(inotify_nullfs);
+ATF_TC_HEAD(inotify_nullfs, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(inotify_nullfs, tc)
+{
+ char path[PATH_MAX], *p;
+ int dfd, error, fd, ifd, mask, wd;
+
+ mask = IN_CREATE | IN_OPEN;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ strlcpy(path, "./test.XXXXXX", sizeof(path));
+ p = mkdtemp(path);
+ ATF_REQUIRE(p == path);
+
+ error = mkdir("./mnt", 0755);
+ ATF_REQUIRE(error == 0);
+
+ /* Mount the testdir onto ./mnt. */
+ mount_nullfs("./mnt", path);
+
+ wd = inotify_add_watch(ifd, "./mnt", mask);
+ ATF_REQUIRE(wd != -1);
+
+ /* Create a file in the lower directory and open it. */
+ dfd = open(path, O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(dfd != -1);
+ fd = openat(dfd, "file", O_RDWR | O_CREAT, 0644);
+ close_checked(fd);
+ close_checked(dfd);
+
+ /* We should see events via the nullfs mount. */
+ consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL);
+ consume_event(ifd, wd, IN_CREATE, 0, "file");
+ consume_event(ifd, wd, IN_OPEN, 0, "file");
+
+ error = inotify_rm_watch(ifd, wd);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, 0, IN_IGNORED, NULL);
+
+ /* Watch the lower directory. */
+ wd = inotify_add_watch(ifd, path, mask);
+ ATF_REQUIRE(wd != -1);
+ /* ... and create a file in the upper directory and open it. */
+ dfd = open("./mnt", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(dfd != -1);
+ fd = openat(dfd, "file2", O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ close_checked(dfd);
+
+ /* We should see events via the lower directory. */
+ consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL);
+ consume_event(ifd, wd, IN_CREATE, 0, "file2");
+ consume_event(ifd, wd, IN_OPEN, 0, "file2");
+
+ close_inotify(ifd);
+}
+ATF_TC_CLEANUP(inotify_nullfs, tc)
+{
+ int error;
+
+ error = unmount("./mnt", 0);
+ if (error != 0) {
+ perror("unmount");
+ exit(1);
+ }
+}
+
+/*
+ * Make sure that exceeding max_events pending events results in an overflow
+ * event.
+ */
+ATF_TC_WITHOUT_HEAD(inotify_queue_overflow);
+ATF_TC_BODY(inotify_queue_overflow, tc)
+{
+ char path[PATH_MAX];
+ size_t size;
+ int error, dfd, ifd, max, wd;
+
+ size = sizeof(max);
+ error = sysctlbyname("vfs.inotify.max_queued_events", &max, &size, NULL,
+ 0);
+ ATF_REQUIRE(error == 0);
+
+ ifd = inotify(IN_NONBLOCK);
+
+ /* Create a directory and watch it for file creation events. */
+ wd = watch_dir(ifd, IN_CREATE, path);
+ dfd = open(path, O_DIRECTORY);
+ ATF_REQUIRE(dfd != -1);
+ /* Generate max+1 file creation events. */
+ for (int i = 0; i < max + 1; i++) {
+ char name[NAME_MAX];
+ int fd;
+
+ (void)snprintf(name, sizeof(name), "file%d", i);
+ fd = openat(dfd, name, O_CREAT | O_RDWR, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ }
+
+ /*
+ * Read our events. We should see files 0..max-1 and then an overflow
+ * event.
+ */
+ for (int i = 0; i < max; i++) {
+ char name[NAME_MAX];
+
+ (void)snprintf(name, sizeof(name), "file%d", i);
+ consume_event(ifd, wd, IN_CREATE, 0, name);
+ }
+
+ /* Look for an overflow event. */
+ consume_event(ifd, -1, 0, IN_Q_OVERFLOW, NULL);
+
+ close_checked(dfd);
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_access_file);
+ATF_TC_BODY(inotify_event_access_file, tc)
+{
+ char path[PATH_MAX], buf[16];
+ off_t nb;
+ ssize_t n;
+ int error, fd, fd1, ifd, s[2], wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_file(ifd, IN_ACCESS, path);
+
+ fd = open(path, O_RDWR);
+ n = write(fd, "test", 4);
+ ATF_REQUIRE(n == 4);
+
+ /* A simple read(2) should generate an access. */
+ ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0);
+ n = read(fd, buf, sizeof(buf));
+ ATF_REQUIRE(n == 4);
+ ATF_REQUIRE(memcmp(buf, "test", 4) == 0);
+ consume_event(ifd, wd, IN_ACCESS, 0, NULL);
+
+ /* copy_file_range(2) should as well. */
+ ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0);
+ fd1 = open("sink", O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd1 != -1);
+ n = copy_file_range(fd, NULL, fd1, NULL, 4, 0);
+ ATF_REQUIRE(n == 4);
+ close_checked(fd1);
+ consume_event(ifd, wd, IN_ACCESS, 0, NULL);
+
+ /* As should sendfile(2). */
+ error = socketpair(AF_UNIX, SOCK_STREAM, 0, s);
+ ATF_REQUIRE(error == 0);
+ error = sendfile(fd, s[0], 0, 4, NULL, &nb, 0);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE(nb == 4);
+ consume_event(ifd, wd, IN_ACCESS, 0, NULL);
+ close_checked(s[0]);
+ close_checked(s[1]);
+
+ close_checked(fd);
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_access_dir);
+ATF_TC_BODY(inotify_event_access_dir, tc)
+{
+ char root[PATH_MAX], path[PATH_MAX];
+ struct dirent *ent;
+ DIR *dir;
+ int error, ifd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_dir(ifd, IN_ACCESS, root);
+ snprintf(path, sizeof(path), "%s/dir", root);
+ error = mkdir(path, 0755);
+ ATF_REQUIRE(error == 0);
+
+ /* Read an entry and generate an access. */
+ dir = opendir(path);
+ ATF_REQUIRE(dir != NULL);
+ ent = readdir(dir);
+ ATF_REQUIRE(ent != NULL);
+ ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0);
+ ATF_REQUIRE(closedir(dir) == 0);
+ consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, "dir");
+
+ /*
+ * Reading the watched directory should generate an access event.
+ * This is contrary to Linux's inotify man page, which states that
+ * IN_ACCESS is only generated for accesses to objects in a watched
+ * directory.
+ */
+ dir = opendir(root);
+ ATF_REQUIRE(dir != NULL);
+ ent = readdir(dir);
+ ATF_REQUIRE(ent != NULL);
+ ATF_REQUIRE(strcmp(ent->d_name, ".") == 0 ||
+ strcmp(ent->d_name, "..") == 0);
+ ATF_REQUIRE(closedir(dir) == 0);
+ consume_event(ifd, wd, IN_ACCESS, IN_ISDIR, NULL);
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_attrib);
+ATF_TC_BODY(inotify_event_attrib, tc)
+{
+ char path[PATH_MAX];
+ int error, ifd, fd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_file(ifd, IN_ATTRIB, path);
+
+ fd = open(path, O_RDWR);
+ ATF_REQUIRE(fd != -1);
+ error = fchmod(fd, 0600);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_ATTRIB, 0, NULL);
+
+ error = fchown(fd, getuid(), getgid());
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_ATTRIB, 0, NULL);
+
+ close_checked(fd);
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_close_nowrite);
+ATF_TC_BODY(inotify_event_close_nowrite, tc)
+{
+ char file[PATH_MAX], file1[PATH_MAX], dir[PATH_MAX];
+ int ifd, fd, wd1, wd2;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd1 = watch_dir(ifd, IN_CLOSE_NOWRITE, dir);
+ wd2 = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, file);
+
+ fd = open(dir, O_DIRECTORY);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd1, IN_CLOSE_NOWRITE, IN_ISDIR, NULL);
+
+ fd = open(file, O_RDONLY);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd2, IN_CLOSE_NOWRITE, 0, NULL);
+
+ snprintf(file1, sizeof(file1), "%s/file", dir);
+ fd = open(file1, O_RDONLY | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd1, IN_CLOSE_NOWRITE, 0, "file");
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_close_write);
+ATF_TC_BODY(inotify_event_close_write, tc)
+{
+ char path[PATH_MAX];
+ int ifd, fd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_file(ifd, IN_CLOSE_NOWRITE | IN_CLOSE_WRITE, path);
+
+ fd = open(path, O_RDWR);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd, IN_CLOSE_WRITE, 0, NULL);
+
+ close_inotify(ifd);
+}
+
+/* Verify that various operations in a directory generate IN_CREATE events. */
+ATF_TC_WITHOUT_HEAD(inotify_event_create);
+ATF_TC_BODY(inotify_event_create, tc)
+{
+ struct sockaddr_un sun;
+ char path[PATH_MAX], path1[PATH_MAX], root[PATH_MAX];
+ ssize_t n;
+ int error, ifd, ifd1, fd, s, wd, wd1;
+ char b;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_dir(ifd, IN_CREATE, root);
+
+ /* Regular file. */
+ snprintf(path, sizeof(path), "%s/file", root);
+ fd = open(path, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ /*
+ * Make sure we get an event triggered by the fd used to create the
+ * file.
+ */
+ ifd1 = inotify(IN_NONBLOCK);
+ wd1 = inotify_add_watch(ifd1, root, IN_MODIFY);
+ b = 42;
+ n = write(fd, &b, sizeof(b));
+ ATF_REQUIRE(n == sizeof(b));
+ close_checked(fd);
+ consume_event(ifd, wd, IN_CREATE, 0, "file");
+ consume_event(ifd1, wd1, IN_MODIFY, 0, "file");
+ close_inotify(ifd1);
+
+ /* Hard link. */
+ snprintf(path1, sizeof(path1), "%s/link", root);
+ error = link(path, path1);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_CREATE, 0, "link");
+
+ /* Directory. */
+ snprintf(path, sizeof(path), "%s/dir", root);
+ error = mkdir(path, 0755);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_CREATE, IN_ISDIR, "dir");
+
+ /* Symbolic link. */
+ snprintf(path1, sizeof(path1), "%s/symlink", root);
+ error = symlink(path, path1);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_CREATE, 0, "symlink");
+
+ /* FIFO. */
+ snprintf(path, sizeof(path), "%s/fifo", root);
+ error = mkfifo(path, 0644);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_CREATE, 0, "fifo");
+
+ /* Binding a socket. */
+ s = socket(AF_UNIX, SOCK_STREAM, 0);
+ memset(&sun, 0, sizeof(sun));
+ sun.sun_family = AF_UNIX;
+ sun.sun_len = sizeof(sun);
+ snprintf(sun.sun_path, sizeof(sun.sun_path), "%s/socket", root);
+ error = bind(s, (struct sockaddr *)&sun, sizeof(sun));
+ ATF_REQUIRE(error == 0);
+ close_checked(s);
+ consume_event(ifd, wd, IN_CREATE, 0, "socket");
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_delete);
+ATF_TC_BODY(inotify_event_delete, tc)
+{
+ char root[PATH_MAX], path[PATH_MAX], file[PATH_MAX];
+ int error, fd, ifd, wd, wd2;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_dir(ifd, IN_DELETE | IN_DELETE_SELF, root);
+
+ snprintf(path, sizeof(path), "%s/file", root);
+ fd = open(path, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ error = unlink(path);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_DELETE, 0, "file");
+ close_checked(fd);
+
+ /*
+ * Make sure that renaming over a file generates a delete event when and
+ * only when that file is watched.
+ */
+ fd = open(path, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ wd2 = inotify_add_watch(ifd, path, IN_DELETE | IN_DELETE_SELF);
+ ATF_REQUIRE(wd2 != -1);
+ snprintf(file, sizeof(file), "%s/file2", root);
+ fd = open(file, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ error = rename(file, path);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd2, IN_DELETE_SELF, 0, NULL);
+ consume_event(ifd, wd2, 0, IN_IGNORED, NULL);
+
+ error = unlink(path);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_DELETE, 0, "file");
+ error = rmdir(root);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd, IN_DELETE_SELF, IN_ISDIR, NULL);
+ consume_event(ifd, wd, 0, IN_IGNORED, NULL);
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_move);
+ATF_TC_BODY(inotify_event_move, tc)
+{
+ char dir1[PATH_MAX], dir2[PATH_MAX], path1[PATH_MAX], path2[PATH_MAX];
+ char path3[PATH_MAX];
+ int error, ifd, fd, wd1, wd2, wd3;
+ uint32_t cookie1, cookie2;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd1 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir1);
+ wd2 = watch_dir(ifd, IN_MOVE | IN_MOVE_SELF, dir2);
+
+ snprintf(path1, sizeof(path1), "%s/file", dir1);
+ fd = open(path1, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ snprintf(path2, sizeof(path2), "%s/file2", dir2);
+ error = rename(path1, path2);
+ ATF_REQUIRE(error == 0);
+ cookie1 = consume_event_cookie(ifd, wd1, IN_MOVED_FROM, 0, "file");
+ cookie2 = consume_event_cookie(ifd, wd2, IN_MOVED_TO, 0, "file2");
+ ATF_REQUIRE_MSG(cookie1 == cookie2,
+ "expected cookie %u, got %u", cookie1, cookie2);
+
+ snprintf(path2, sizeof(path2), "%s/dir", dir2);
+ error = rename(dir1, path2);
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd1, IN_MOVE_SELF, IN_ISDIR, NULL);
+ consume_event(ifd, wd2, IN_MOVED_TO, IN_ISDIR, "dir");
+
+ wd3 = watch_file(ifd, IN_MOVE_SELF, path3);
+ error = rename(path3, "foo");
+ ATF_REQUIRE(error == 0);
+ consume_event(ifd, wd3, IN_MOVE_SELF, 0, NULL);
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITHOUT_HEAD(inotify_event_open);
+ATF_TC_BODY(inotify_event_open, tc)
+{
+ char root[PATH_MAX], path[PATH_MAX];
+ int error, ifd, fd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ wd = watch_dir(ifd, IN_OPEN, root);
+
+ snprintf(path, sizeof(path), "%s/file", root);
+ fd = open(path, O_RDWR | O_CREAT, 0644);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd, IN_OPEN, 0, "file");
+
+ fd = open(path, O_PATH);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd, IN_OPEN, 0, "file");
+
+ fd = open(root, O_DIRECTORY);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL);
+
+ snprintf(path, sizeof(path), "%s/fifo", root);
+ error = mkfifo(path, 0644);
+ ATF_REQUIRE(error == 0);
+ fd = open(path, O_RDWR);
+ ATF_REQUIRE(fd != -1);
+ close_checked(fd);
+ consume_event(ifd, wd, IN_OPEN, 0, "fifo");
+
+ close_inotify(ifd);
+}
+
+ATF_TC_WITH_CLEANUP(inotify_event_unmount);
+ATF_TC_HEAD(inotify_event_unmount, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(inotify_event_unmount, tc)
+{
+ int error, fd, ifd, wd;
+
+ ifd = inotify(IN_NONBLOCK);
+
+ error = mkdir("./root", 0755);
+ ATF_REQUIRE(error == 0);
+
+ mount_tmpfs("./root");
+
+ error = mkdir("./root/dir", 0755);
+ ATF_REQUIRE(error == 0);
+ wd = inotify_add_watch(ifd, "./root/dir", IN_OPEN);
+ ATF_REQUIRE(wd >= 0);
+
+ fd = open("./root/dir", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE(fd != -1);
+ consume_event(ifd, wd, IN_OPEN, IN_ISDIR, NULL);
+ close_checked(fd);
+
+ /* A regular unmount should fail, as inotify holds a vnode reference. */
+ error = unmount("./root", 0);
+ ATF_REQUIRE_ERRNO(EBUSY, error == -1);
+ error = unmount("./root", MNT_FORCE);
+ ATF_REQUIRE_MSG(error == 0,
+ "unmounting ./root failed: %s", strerror(errno));
+
+ consume_event(ifd, wd, 0, IN_UNMOUNT, NULL);
+ consume_event(ifd, wd, 0, IN_IGNORED, NULL);
+
+ close_inotify(ifd);
+}
+ATF_TC_CLEANUP(inotify_event_unmount, tc)
+{
+ (void)unmount("./root", MNT_FORCE);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ /* Tests for the inotify syscalls. */
+ ATF_TP_ADD_TC(tp, inotify_capsicum);
+ ATF_TP_ADD_TC(tp, inotify_coalesce);
+ ATF_TP_ADD_TC(tp, inotify_mask_create);
+ ATF_TP_ADD_TC(tp, inotify_nullfs);
+ ATF_TP_ADD_TC(tp, inotify_queue_overflow);
+ /* Tests for the various inotify event types. */
+ ATF_TP_ADD_TC(tp, inotify_event_access_file);
+ ATF_TP_ADD_TC(tp, inotify_event_access_dir);
+ ATF_TP_ADD_TC(tp, inotify_event_attrib);
+ ATF_TP_ADD_TC(tp, inotify_event_close_nowrite);
+ ATF_TP_ADD_TC(tp, inotify_event_close_write);
+ ATF_TP_ADD_TC(tp, inotify_event_create);
+ ATF_TP_ADD_TC(tp, inotify_event_delete);
+ ATF_TP_ADD_TC(tp, inotify_event_move);
+ ATF_TP_ADD_TC(tp, inotify_event_open);
+ ATF_TP_ADD_TC(tp, inotify_event_unmount);
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/jail_lookup_root.c b/tests/sys/kern/jail_lookup_root.c
new file mode 100644
index 000000000000..34e89f4aea2b
--- /dev/null
+++ b/tests/sys/kern/jail_lookup_root.c
@@ -0,0 +1,133 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org>
+ */
+
+#include <sys/param.h>
+#include <sys/jail.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <jail.h>
+#include <mntopts.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <atf-c.h>
+
+static void
+mkdir_checked(const char *dir, mode_t mode)
+{
+ int error;
+
+ error = mkdir(dir, mode);
+ ATF_REQUIRE_MSG(error == 0 || errno == EEXIST,
+ "mkdir %s: %s", dir, strerror(errno));
+}
+
+static void __unused
+mount_nullfs(const char *dir, const char *target)
+{
+ struct iovec *iov;
+ char errmsg[1024];
+ int error, iovlen;
+
+ iov = NULL;
+ iovlen = 0;
+
+ build_iovec(&iov, &iovlen, __DECONST(char *, "fstype"),
+ __DECONST(char *, "nullfs"), (size_t)-1);
+ build_iovec(&iov, &iovlen, __DECONST(char *, "fspath"),
+ __DECONST(char *, target), (size_t)-1);
+ build_iovec(&iov, &iovlen, __DECONST(char *, "from"),
+ __DECONST(char *, dir), (size_t)-1);
+ build_iovec(&iov, &iovlen, __DECONST(char *, "errmsg"),
+ errmsg, sizeof(errmsg));
+
+ errmsg[0] = '\0';
+ error = nmount(iov, iovlen, 0);
+ ATF_REQUIRE_MSG(error == 0, "nmount: %s",
+ errmsg[0] != '\0' ? errmsg : strerror(errno));
+
+ free_iovec(&iov, &iovlen);
+}
+
+ATF_TC_WITH_CLEANUP(jail_root);
+ATF_TC_HEAD(jail_root, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(jail_root, tc)
+{
+ int error, fd, jid;
+
+ mkdir_checked("./root", 0755);
+ mkdir_checked("./root/a", 0755);
+ mkdir_checked("./root/b", 0755);
+ mkdir_checked("./root/a/c", 0755);
+
+ jid = jail_setv(JAIL_CREATE | JAIL_ATTACH,
+ "name", "nullfs_jail_root_test",
+ "allow.mount", "true",
+ "allow.mount.nullfs", "true",
+ "enforce_statfs", "1",
+ "path", "./root",
+ "persist", NULL,
+ NULL);
+ ATF_REQUIRE_MSG(jid >= 0, "jail_setv: %s", jail_errmsg);
+
+ mount_nullfs("/a", "/b");
+
+ error = chdir("/b/c");
+ ATF_REQUIRE(error == 0);
+
+ error = rename("/a/c", "/c");
+ ATF_REQUIRE(error == 0);
+
+ /* Descending to the jail root should be ok. */
+ error = chdir("..");
+ ATF_REQUIRE(error == 0);
+
+ /* Going beyond the root will trigger an error. */
+ error = chdir("..");
+ ATF_REQUIRE_ERRNO(ENOENT, error != 0);
+ fd = open("..", O_RDONLY | O_DIRECTORY);
+ ATF_REQUIRE_ERRNO(ENOENT, fd < 0);
+}
+ATF_TC_CLEANUP(jail_root, tc)
+{
+ struct statfs fs;
+ fsid_t fsid;
+ int error, jid;
+
+ error = statfs("./root/b", &fs);
+ if (error != 0)
+ err(1, "statfs ./b");
+ fsid = fs.f_fsid;
+ error = statfs("./root", &fs);
+ if (error != 0)
+ err(1, "statfs ./root");
+ if (fsid.val[0] != fs.f_fsid.val[0] ||
+ fsid.val[1] != fs.f_fsid.val[1]) {
+ error = unmount("./root/b", 0);
+ if (error != 0)
+ err(1, "unmount ./root/b");
+ }
+
+ jid = jail_getid("nullfs_jail_root_test");
+ if (jid >= 0) {
+ error = jail_remove(jid);
+ if (error != 0)
+ err(1, "jail_remove");
+ }
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, jail_root);
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/jailmeta.sh b/tests/sys/kern/jailmeta.sh
new file mode 100644
index 000000000000..9a63f958231f
--- /dev/null
+++ b/tests/sys/kern/jailmeta.sh
@@ -0,0 +1,588 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 SkunkWerks GmbH
+#
+# This software was developed by Igor Ostapenko <igoro@FreeBSD.org>
+# under sponsorship from SkunkWerks GmbH.
+#
+
+setup()
+{
+ # Check if we have enough buffer space for testing
+ if [ $(sysctl -n security.jail.meta_maxbufsize) -lt 128 ]; then
+ atf_skip "sysctl security.jail.meta_maxbufsize must be 128+ for testing."
+ fi
+}
+
+atf_test_case "jail_create" "cleanup"
+jail_create_head()
+{
+ atf_set descr 'Test that metadata can be set upon jail creation with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_create_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="a b c" env="C B A"
+
+ atf_check -s exit:0 -o inline:"a b c\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"C B A\n" \
+ jls -jj env
+}
+jail_create_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "jail_modify" "cleanup"
+jail_modify_head()
+{
+ atf_set descr 'Test that metadata can be modified after jail creation with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_modify_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="a b c" env="CAB"
+
+ atf_check -s exit:0 -o inline:"a b c\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"CAB\n" \
+ jls -jj env
+
+ atf_check -s exit:0 \
+ jail -m name=j meta="t1=A t2=B" env="CAB2"
+
+ atf_check -s exit:0 -o inline:"t1=A t2=B\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"CAB2\n" \
+ jls -jj env
+}
+jail_modify_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "jail_add" "cleanup"
+jail_add_head()
+{
+ atf_set descr 'Test that metadata can be added to an existing jail with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_add_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist host.hostname=jail1
+
+ atf_check -s exit:0 -o inline:'""\n' \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:'""\n' \
+ jls -jj env
+
+ atf_check -s exit:0 \
+ jail -m name=j meta="$(jot 3 1 3)" env="$(jot 2 11 12)"
+
+ atf_check -s exit:0 -o inline:"1\n2\n3\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"11\n12\n" \
+ jls -jj env
+}
+jail_add_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "jail_reset" "cleanup"
+jail_reset_head()
+{
+ atf_set descr 'Test that metadata can be reset to an empty string with jail(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jail_reset_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="123" env="456"
+
+ atf_check -s exit:0 -o inline:"123\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"456\n" \
+ jls -jj env
+
+ atf_check -s exit:0 \
+ jail -m name=j meta= env=
+
+ atf_check -s exit:0 -o inline:'""\n' \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:'""\n' \
+ jls -jj env
+}
+jail_reset_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "jls_libxo_json" "cleanup"
+jls_libxo_json_head()
+{
+ atf_set descr 'Test that metadata can be read with jls(8) using libxo JSON'
+ atf_set require.user root
+ atf_set execenv jail
+}
+jls_libxo_json_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="a b c" env="1 2 3"
+
+ atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"name":"j","meta":"a b c"}]}}\n' \
+ jls -jj --libxo json name meta
+ atf_check -s exit:0 -o inline:'{"__version": "2", "jail-information": {"jail": [{"env":"1 2 3"}]}}\n' \
+ jls -jj --libxo json env
+}
+jls_libxo_json_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "flua_create" "cleanup"
+flua_create_head()
+{
+ atf_set descr 'Test that metadata can be set upon jail creation with flua'
+ atf_set require.user root
+ atf_set execenv jail
+}
+flua_create_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v2", ["env"]="BAC", ["persist"]="true"}, jail.CREATE)'
+
+ atf_check -s exit:0 -o inline:"t1 t2=v2\n" \
+ /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"meta"}); print(res["meta"])'
+ atf_check -s exit:0 -o inline:"BAC\n" \
+ /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"env"}); print(res["env"])'
+}
+flua_create_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "flua_modify" "cleanup"
+flua_modify_head()
+{
+ atf_set descr 'Test that metadata can be changed with flua after jail creation'
+ atf_set require.user root
+ atf_set execenv jail
+}
+flua_modify_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="ABC" env="123"
+
+ atf_check -s exit:0 -o inline:"ABC\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"123\n" \
+ jls -jj env
+
+ atf_check -s exit:0 \
+ /usr/libexec/flua -ljail -e 'jail.setparams("j", {["meta"]="t1 t2=v", ["env"]="4"}, jail.UPDATE)'
+
+ atf_check -s exit:0 -o inline:"t1 t2=v\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"4\n" \
+ jls -jj env
+}
+flua_modify_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "env_readable_by_jail" "cleanup"
+env_readable_by_jail_head()
+{
+ atf_set descr 'Test that a jail can read its own env parameter via sysctl(8)'
+ atf_set require.user root
+ atf_set execenv jail
+}
+env_readable_by_jail_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -jj
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta="a b c" env="CBA"
+
+ atf_check -s exit:0 -o inline:"a b c\n" \
+ jls -jj meta
+ atf_check -s exit:0 -o inline:"CBA\n" \
+ jls -jj env
+
+ atf_check -s exit:0 -o inline:"CBA\n" \
+ jexec j sysctl -n security.jail.env
+}
+env_readable_by_jail_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "not_inheritable" "cleanup"
+not_inheritable_head()
+{
+ atf_set descr 'Test that a jail does not inherit metadata from its parent jail'
+ atf_set require.user root
+ atf_set execenv jail
+}
+not_inheritable_body()
+{
+ setup
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -j parent
+
+ atf_check -s exit:0 \
+ jail -c name=parent children.max=1 persist meta="abc" env="cba"
+
+ jexec parent jail -c name=child persist
+
+ atf_check -s exit:0 -o inline:"abc\n" \
+ jls -j parent meta
+ atf_check -s exit:0 -o inline:'""\n' \
+ jls -j parent.child meta
+
+ atf_check -s exit:0 -o inline:"cba\n" \
+ jexec parent sysctl -n security.jail.env
+ atf_check -s exit:0 -o inline:"\n" \
+ jexec parent.child sysctl -n security.jail.env
+}
+not_inheritable_cleanup()
+{
+ jail -r parent.child
+ jail -r parent
+ return 0
+}
+
+atf_test_case "maxbufsize" "cleanup"
+maxbufsize_head()
+{
+ atf_set descr 'Test that metadata buffer maximum size can be changed'
+ atf_set require.user root
+ atf_set is.exclusive true
+}
+maxbufsize_body()
+{
+ setup
+
+ jn=jailmeta_maxbufsize
+
+ atf_check -s not-exit:0 -e match:"not found" -o ignore \
+ jls -j $jn
+
+ # the size counts string length and the trailing \0 char
+ origmax=$(sysctl -n security.jail.meta_maxbufsize)
+
+ # must be fine with current max
+ atf_check -s exit:0 \
+ jail -c name=$jn persist meta="$(printf %$((origmax-1))s)"
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn meta | wc -c
+ #
+ atf_check -s exit:0 \
+ jail -m name=$jn env="$(printf %$((origmax-1))s)"
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn env | wc -c
+
+ # should not allow exceeding current max
+ atf_check -s not-exit:0 -e match:"too large" \
+ jail -m name=$jn meta="$(printf %${origmax}s)"
+ #
+ atf_check -s not-exit:0 -e match:"too large" \
+ jail -m name=$jn env="$(printf %${origmax}s)"
+
+ # should allow the same size with increased max
+ newmax=$((origmax + 1))
+ sysctl security.jail.meta_maxbufsize=$newmax
+ atf_check -s exit:0 \
+ jail -m name=$jn meta="$(printf %${origmax}s)"
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn meta | wc -c
+ #
+ atf_check -s exit:0 \
+ jail -m name=$jn env="$(printf %${origmax}s)"
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn env | wc -c
+
+ # decrease back to the original max
+ sysctl security.jail.meta_maxbufsize=$origmax
+ atf_check -s not-exit:0 -e match:"too large" \
+ jail -m name=$jn meta="$(printf %${origmax}s)"
+ #
+ atf_check -s not-exit:0 -e match:"too large" \
+ jail -m name=$jn env="$(printf %${origmax}s)"
+
+ # the previously set long meta is still readable as is
+ # due to the soft limit remains higher than the hard limit
+ atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.meta)'
+ atf_check_equal '${newmax}' '$(sysctl -n security.jail.param.env)'
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn meta | wc -c
+ #
+ atf_check -s exit:0 -o inline:"${origmax}\n" \
+ jls -j $jn env | wc -c
+}
+maxbufsize_cleanup()
+{
+ jail -r jailmeta_maxbufsize
+ return 0
+}
+
+atf_test_case "keyvalue" "cleanup"
+keyvalue_head()
+{
+ atf_set descr 'Test that metadata can be handled as a set of key=value\n strings using jail(8), jls(8), and flua'
+ atf_set require.user root
+ atf_set execenv jail
+}
+keyvalue_generic()
+{
+ local meta=$1
+
+ atf_check -sexit:0 -oinline:'""\n' jls -jj $meta
+
+ # Note: each sub-case depends on the results of the previous ones
+
+ # Should be able to extract a key added manually
+ atf_check -sexit:0 jail -m name=j $meta="a=1"
+ atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta
+ atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a
+ atf_check -sexit:0 jail -m name=j $meta="$(printf 'a=2\nb=3')"
+ atf_check -sexit:0 -oinline:'a=2\nb=3\n' jls -jj $meta
+ atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.a
+ atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.b
+
+ # Should provide nothing for a non-found key
+ atf_check -sexit:0 -oinline:'\n' jls -jj $meta.c
+
+ # Should be able to lookup multiple keys at once
+ atf_check -sexit:0 -oinline:'3 2\n' jls -jj $meta.b $meta.a
+
+ # Should be able to lookup keys and the whole buffer at once
+ atf_check -sexit:0 -oinline:'3 a=2\nb=3 2\n' jls -jj $meta.b $meta $meta.a
+
+ # Should be able to lookup a key using libxo-based JSON output
+ s='{"__version": "2", "jail-information": {"jail": [{"'$meta'.b":"3"}]}}\n'
+ atf_check -s exit:0 -o inline:"$s" jls -jj --libxo json $meta.b
+
+ # Should provide nothing for a non-found key using libxo-based JSON output
+ s='{"__version": "2", "jail-information": {"jail": [{}]}}\n'
+ atf_check -s exit:0 -o inline:"$s" jls -jj --libxo json $meta.c $meta.d
+
+ # Should be able to lookup a key using flua
+ atf_check -s exit:0 -o inline:"2\n" \
+ /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"'$meta'.a"}); print(res["'$meta'.a"])'
+
+ # Should provide nil for a non-found key using flua
+ atf_check -s exit:0 -o inline:"true\n" \
+ /usr/libexec/flua -ljail -e 'jid, res = jail.getparams("j", {"'$meta'.meta"}); print(res["'$meta'.meta"] == nil)'
+
+ # Should allow resetting a buffer
+ atf_check -sexit:0 jail -m name=j $meta=
+ atf_check -sexit:0 -oinline:' "" \n' jls -jj $meta.c $meta $meta.a
+
+ # Should allow adding a new key
+ atf_check -sexit:0 jail -m name=j $meta.a=1
+ atf_check -sexit:0 -oinline:'1\n' jls -jj $meta.a
+ atf_check -sexit:0 -oinline:'a=1\n' jls -jj $meta
+
+ # Should allow adding multiple new keys at once
+ atf_check -sexit:0 jail -m name=j $meta.c=3 $meta.b=2
+ atf_check -sexit:0 -oinline:'3\n' jls -jj $meta.c
+ atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b
+ atf_check -sexit:0 -oinline:'b=2\nc=3\na=1\n' jls -jj $meta
+
+ # Should replace existing keys
+ atf_check -sexit:0 jail -m name=j $meta.a=A $meta.c=C
+ atf_check -sexit:0 -oinline:'A\n' jls -jj $meta.a
+ atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c
+ atf_check -sexit:0 -oinline:'c=C\na=A\nb=2\n' jls -jj $meta
+
+ # Should treat empty value correctly
+ atf_check -sexit:0 jail -m name=j $meta.a=
+ atf_check -sexit:0 -oinline:'""\n' jls -jj $meta.a
+ atf_check -sexit:0 -oinline:'a=\nc=C\nb=2\n' jls -jj $meta
+
+ # Should treat NULL value as a key removal
+ atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b
+ atf_check -sexit:0 jail -m name=j $meta.b
+ atf_check -sexit:0 -oinline:'\n' jls -jj $meta.b
+ atf_check -sexit:0 -oinline:'a=\nc=C\n' jls -jj $meta
+
+ # Should allow changing the whole buffer and per key at once (order matters)
+ atf_check -sexit:0 jail -m name=j $meta.a=1 $meta=ttt $meta.b=2
+ atf_check -sexit:0 -oinline:'\n' jls -jj $meta.a
+ atf_check -sexit:0 -oinline:'2\n' jls -jj $meta.b
+ atf_check -sexit:0 -oinline:'b=2\nttt\n' jls -jj $meta
+
+ # Should treat only the first equal sign as syntax
+ atf_check -sexit:0 jail -m name=j $meta.b==
+ atf_check -sexit:0 -oinline:'=\n' jls -jj $meta.b
+ atf_check -sexit:0 -oinline:'b==\nttt\n' jls -jj $meta
+
+ # Should allow adding or modifying keys using flua
+ atf_check -s exit:0 \
+ /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.b'"]="ttt", ["'$meta'.c"]="C"}, jail.UPDATE)'
+ atf_check -sexit:0 -oinline:'ttt\n' jls -jj $meta.b
+ atf_check -sexit:0 -oinline:'C\n' jls -jj $meta.c
+
+ # Should allow key removal using flua
+ atf_check -s exit:0 \
+ /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.c'"] = {}}, jail.UPDATE)'
+ atf_check -sexit:0 -oinline:'\n' jls -jj $meta.c
+ atf_check -s exit:0 \
+ /usr/libexec/flua -ljail -e 'jail.setparams("j", {["'$meta.b'"] = false}, jail.UPDATE)'
+ atf_check -sexit:0 -oinline:'\n' jls -jj $meta.b
+
+ # Should respectively support "jls -s" for a missing key
+ atf_check -sexit:0 -oinline:''$meta'.missing\n' jls -jj -s $meta.missing
+}
+keyvalue_body()
+{
+ setup
+
+ atf_check -s exit:0 \
+ jail -c name=j persist meta env
+
+ keyvalue_generic "meta"
+ keyvalue_generic "env"
+}
+keyvalue_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_test_case "keyvalue_contention" "cleanup"
+keyvalue_contention_head()
+{
+ atf_set descr 'Try to stress metadata read/write mechanism with some contention'
+ atf_set require.user root
+ atf_set execenv jail
+ atf_set timeout 30
+}
+keyvalue_stresser()
+{
+ local jailname=$1
+ local modifier=$2
+
+ while true
+ do
+ jail -m name=$jailname $modifier
+ done
+}
+keyvalue_contention_body()
+{
+ setup
+
+ atf_check -s exit:0 jail -c name=j persist meta env
+
+ keyvalue_stresser "j" "meta.a=1" &
+ apid=$!
+ keyvalue_stresser "j" "meta.b=2" &
+ bpid=$!
+ keyvalue_stresser "j" "env.c=3" &
+ cpid=$!
+ keyvalue_stresser "j" "env.d=4" &
+ dpid=$!
+
+ for it in $(jot 8)
+ do
+ jail -m name=j meta='meta=META' env='env=ENV'
+ sleep 1
+ atf_check -sexit:0 -oinline:'1\n' jls -jj meta.a
+ atf_check -sexit:0 -oinline:'2\n' jls -jj meta.b
+ atf_check -sexit:0 -oinline:'3\n' jls -jj env.c
+ atf_check -sexit:0 -oinline:'4\n' jls -jj env.d
+ atf_check -sexit:0 -oinline:'META\n' jls -jj meta.meta
+ atf_check -sexit:0 -oinline:'ENV\n' jls -jj env.env
+ done
+
+ # TODO: Think of adding a stresser on the kernel side which does
+ # osd_set() w/o allprison lock. It could test the compare
+ # and swap mechanism in jm_osd_method_set().
+
+ kill -9 $apid $bpid $cpid $dpid
+}
+keyvalue_contention_cleanup()
+{
+ jail -r j
+ return 0
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "jail_create"
+ atf_add_test_case "jail_modify"
+ atf_add_test_case "jail_add"
+ atf_add_test_case "jail_reset"
+
+ atf_add_test_case "jls_libxo_json"
+
+ atf_add_test_case "flua_create"
+ atf_add_test_case "flua_modify"
+
+ atf_add_test_case "env_readable_by_jail"
+ atf_add_test_case "not_inheritable"
+
+ atf_add_test_case "maxbufsize"
+
+ atf_add_test_case "keyvalue"
+ atf_add_test_case "keyvalue_contention"
+}
diff --git a/tests/sys/kern/kern_copyin.c b/tests/sys/kern/kern_copyin.c
index 6ad195d5d055..e677081a5c70 100644
--- a/tests/sys/kern/kern_copyin.c
+++ b/tests/sys/kern/kern_copyin.c
@@ -1,6 +1,5 @@
/*-
* Copyright (c) 2015, 2020 The FreeBSD Foundation
- * All rights reserved.
*
* This software was developed by Konstantin Belousov <kib@FreeBSD.org>
* under sponsorship from the FreeBSD Foundation.
diff --git a/tests/sys/kern/ktls_test.c b/tests/sys/kern/ktls_test.c
index f57ae74112a2..72497196b945 100644
--- a/tests/sys/kern/ktls_test.c
+++ b/tests/sys/kern/ktls_test.c
@@ -2812,7 +2812,7 @@ ATF_TC_BODY(ktls_listening_socket, tc)
TLS_MINOR_VER_THREE, (uint64_t)random(), &en);
ATF_REQUIRE_ERRNO(ENOTCONN,
setsockopt(s, IPPROTO_TCP, TCP_TXTLS_ENABLE, &en, sizeof(en)) != 0);
- ATF_REQUIRE_ERRNO(EINVAL,
+ ATF_REQUIRE_ERRNO(ENOTCONN,
setsockopt(s, IPPROTO_TCP, TCP_RXTLS_ENABLE, &en, sizeof(en)) != 0);
ATF_REQUIRE(close(s) == 0);
}
diff --git a/tests/sys/kern/ktrace_test.c b/tests/sys/kern/ktrace_test.c
index 21868441c687..785c78bedaba 100644
--- a/tests/sys/kern/ktrace_test.c
+++ b/tests/sys/kern/ktrace_test.c
@@ -33,6 +33,7 @@
#include <sys/ktrace.h>
#include <sys/mman.h>
#include <sys/socket.h>
+#include <sys/syscall.h>
#include <sys/sysent.h>
#include <sys/time.h>
#include <sys/uio.h>
@@ -86,6 +87,16 @@ child_fail_require(const char *file, int line, const char *fmt, ...)
_exit(32);
}
+static void *
+xmalloc(size_t size)
+{
+ void *p;
+
+ p = malloc(size);
+ ATF_REQUIRE(p != NULL);
+ return (p);
+}
+
/*
* Determine sysdecode ABI based on proc's ABI in sv_flags.
*/
@@ -105,36 +116,47 @@ syscallabi(u_int sv_flags)
return (SYSDECODE_ABI_UNKNOWN);
}
-/*
- * Start tracing capability violations and notify child that it can execute.
- * Return @numv capability violations from child in @v.
- */
-static void
-cap_trace_child(int cpid, struct ktr_cap_fail *v, int numv)
+static int
+trace_child(int cpid, int facility, int status)
{
- struct ktr_header header;
- int error, fd, i;
+ int error, fd;
ATF_REQUIRE((fd = open("ktrace.out",
- O_RDONLY | O_CREAT | O_TRUNC)) != -1);
- ATF_REQUIRE(ktrace("ktrace.out", KTROP_SET,
- KTRFAC_CAPFAIL, cpid) != -1);
+ O_RDONLY | O_CREAT | O_TRUNC, 0600)) != -1);
+ ATF_REQUIRE_MSG(ktrace("ktrace.out", KTROP_SET, facility, cpid) != -1,
+ "ktrace failed: %s", strerror(errno));
/* Notify child that we've starting tracing. */
ATF_REQUIRE(kill(cpid, SIGUSR1) != -1);
/* Wait for child to raise violation and exit. */
ATF_REQUIRE(waitpid(cpid, &error, 0) != -1);
ATF_REQUIRE(WIFEXITED(error));
- ATF_REQUIRE_EQ(WEXITSTATUS(error), 0);
+ ATF_REQUIRE_EQ(WEXITSTATUS(error), status);
+ return (fd);
+}
+
+/*
+ * Start tracing capability violations and notify child that it can execute.
+ * Return @numv capability violations from child in @v.
+ */
+static void
+cap_trace_child(pid_t cpid, struct ktr_cap_fail *v, int numv)
+{
+ struct ktr_header header;
+ ssize_t n;
+ int fd;
+
+ fd = trace_child(cpid, KTRFAC_CAPFAIL, 0);
+
/* Read ktrace header and ensure violation occurred. */
- for (i = 0; i < numv; ++i) {
- ATF_REQUIRE((error = read(fd, &header, sizeof(header))) != -1);
- ATF_REQUIRE_EQ(error, sizeof(header));
+ for (int i = 0; i < numv; ++i) {
+ ATF_REQUIRE((n = read(fd, &header, sizeof(header))) != -1);
+ ATF_REQUIRE_EQ(n, sizeof(header));
ATF_REQUIRE_EQ(header.ktr_len, sizeof(*v));
ATF_REQUIRE_EQ(header.ktr_pid, cpid);
/* Read the capability violation. */
- ATF_REQUIRE((error = read(fd, v + i,
+ ATF_REQUIRE((n = read(fd, v + i,
sizeof(*v))) != -1);
- ATF_REQUIRE_EQ(error, sizeof(*v));
+ ATF_REQUIRE_EQ(n, sizeof(*v));
}
ATF_REQUIRE(close(fd) != -1);
}
@@ -301,7 +323,11 @@ ATF_TC_BODY(ktrace__cap_signal, tc)
* Test if opening a socket with a restricted protocol is reported
* as a protocol violation.
*/
-ATF_TC_WITHOUT_HEAD(ktrace__cap_proto);
+ATF_TC(ktrace__cap_proto);
+ATF_TC_HEAD(ktrace__cap_proto, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
ATF_TC_BODY(ktrace__cap_proto, tc)
{
struct ktr_cap_fail violation;
@@ -352,12 +378,11 @@ ATF_TC_BODY(ktrace__cap_sockaddr, tc)
ATF_REQUIRE(sigaddset(&set, SIGUSR1) != -1);
ATF_REQUIRE(sigprocmask(SIG_BLOCK, &set, NULL) != -1);
- CHILD_REQUIRE((sfd = socket(AF_INET, SOCK_DGRAM,
- IPPROTO_UDP)) != -1);
+ ATF_REQUIRE((sfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) != -1);
addr.sin_family = AF_INET;
addr.sin_port = htons(5000);
- addr.sin_addr.s_addr = INADDR_ANY;
- CHILD_REQUIRE(bind(sfd, (const struct sockaddr *)&addr,
+ addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ ATF_REQUIRE(bind(sfd, (const struct sockaddr *)&addr,
sizeof(addr)) != -1);
ATF_REQUIRE((pid = fork()) != -1);
@@ -383,7 +408,7 @@ ATF_TC_BODY(ktrace__cap_sockaddr, tc)
saddr = (struct sockaddr_in *)&violation.cap_data.cap_sockaddr;
ATF_REQUIRE_EQ(saddr->sin_family, AF_INET);
ATF_REQUIRE_EQ(saddr->sin_port, htons(5000));
- ATF_REQUIRE_EQ(saddr->sin_addr.s_addr, INADDR_ANY);
+ ATF_REQUIRE_EQ(saddr->sin_addr.s_addr, htonl(INADDR_LOOPBACK));
close(sfd);
}
@@ -507,6 +532,78 @@ ATF_TC_BODY(ktrace__cap_shm_open, tc)
ATF_REQUIRE_STREQ(violation.cap_data.cap_path, "/ktrace_shm");
}
+/*
+ * Make sure that ktrace is disabled upon exec of a setuid binary.
+ */
+ATF_TC(ktrace__setuid_exec);
+ATF_TC_HEAD(ktrace__setuid_exec, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(ktrace__setuid_exec, tc)
+{
+ struct ktr_header header;
+ struct ktr_syscall *syscall;
+ sigset_t set = { };
+ off_t off, off1;
+ ssize_t n;
+ pid_t pid;
+ int error, fd;
+
+ /* Block SIGUSR1 so child does not terminate. */
+ ATF_REQUIRE(sigaddset(&set, SIGUSR1) != -1);
+ ATF_REQUIRE(sigprocmask(SIG_BLOCK, &set, NULL) != -1);
+
+ ATF_REQUIRE((pid = fork()) != -1);
+ if (pid == 0) {
+ /* Wait until ktrace has started. */
+ CHILD_REQUIRE(sigwait(&set, &error) != -1);
+ CHILD_REQUIRE_EQ(error, SIGUSR1);
+
+ execve("/usr/bin/su", (char *[]){ "su", "whoami", NULL }, NULL);
+ _exit(0);
+ }
+
+ fd = trace_child(pid, KTRFAC_SYSCALL, 1);
+
+ n = read(fd, &header, sizeof(header));
+ ATF_REQUIRE(n >= 0);
+ ATF_REQUIRE_EQ((size_t)n, sizeof(header));
+ ATF_REQUIRE_EQ(header.ktr_pid, pid);
+ ATF_REQUIRE(header.ktr_len >= (int)sizeof(*syscall));
+
+ syscall = xmalloc(header.ktr_len);
+ n = read(fd, syscall, header.ktr_len);
+ ATF_REQUIRE(n >= 0);
+ ATF_REQUIRE_EQ(n, header.ktr_len);
+ if (syscall->ktr_code == SYS_sigwait) {
+ free(syscall);
+
+ /* Skip the sigwait() syscall. */
+ n = read(fd, &header, sizeof(header));
+ ATF_REQUIRE(n >= 0);
+ ATF_REQUIRE_EQ((size_t)n, sizeof(header));
+ ATF_REQUIRE_EQ(header.ktr_pid, pid);
+ ATF_REQUIRE(header.ktr_len >= (int)sizeof(*syscall));
+
+ syscall = xmalloc(header.ktr_len);
+ n = read(fd, syscall, header.ktr_len);
+ ATF_REQUIRE(n >= 0);
+ ATF_REQUIRE_EQ(n, header.ktr_len);
+ }
+ ATF_REQUIRE_EQ(syscall->ktr_code, SYS_execve);
+ free(syscall);
+
+ /* su is setuid root, so this should have been the last entry. */
+ off = lseek(fd, 0, SEEK_CUR);
+ ATF_REQUIRE(off != -1);
+ off1 = lseek(fd, 0, SEEK_END);
+ ATF_REQUIRE(off1 != -1);
+ ATF_REQUIRE_EQ(off, off1);
+
+ ATF_REQUIRE(close(fd) == 0);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, ktrace__cap_not_capable);
@@ -518,5 +615,6 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, ktrace__cap_namei);
ATF_TP_ADD_TC(tp, ktrace__cap_cpuset);
ATF_TP_ADD_TC(tp, ktrace__cap_shm_open);
+ ATF_TP_ADD_TC(tp, ktrace__setuid_exec);
return (atf_no_error());
}
diff --git a/tests/sys/kern/logsigexit_test.sh b/tests/sys/kern/logsigexit_test.sh
new file mode 100644
index 000000000000..f0db02613533
--- /dev/null
+++ b/tests/sys/kern/logsigexit_test.sh
@@ -0,0 +1,37 @@
+#
+# Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+atf_test_case basic
+basic_body()
+{
+
+ if ! dmesg >/dev/null 2>&1; then
+ atf_skip "No dmesg(8) access"
+ fi
+
+ # SIGABRT carefully chosen to avoid issues when run under Kyua. No
+ # matter the value of the global kern.logsigexit, these should force
+ # the messages as appropriate and we'll all be happy.
+ proccontrol -m logsigexit -s enable \
+ sh -c 'echo $$ > enabled.out; kill -ABRT $$'
+ proccontrol -m logsigexit -s disable \
+ sh -c 'echo $$ > disabled.out; kill -ABRT $$'
+
+ atf_check test -s enabled.out
+ atf_check test -s disabled.out
+
+ read enpid < enabled.out
+ read dispid < disabled.out
+
+ atf_check -o save:dmesg.out dmesg
+ atf_check grep -Eq "$enpid.+exited on signal" dmesg.out
+ atf_check -s not-exit:0 grep -Eq "$dispid.+exited on signal" dmesg.out
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case basic
+}
diff --git a/tests/sys/kern/pipe/Makefile b/tests/sys/kern/pipe/Makefile
index e9bc0b048c5b..e8fa05ee8e0e 100644
--- a/tests/sys/kern/pipe/Makefile
+++ b/tests/sys/kern/pipe/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/kern/pipe
PLAIN_TESTS_C+= big_pipe_test
diff --git a/tests/sys/kern/prace.c b/tests/sys/kern/prace.c
new file mode 100644
index 000000000000..e6aa09ec2180
--- /dev/null
+++ b/tests/sys/kern/prace.c
@@ -0,0 +1,144 @@
+/*-
+ * Copyright (c) 2024 Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * These tests demonstrate a bug in ppoll() and pselect() where a blocked
+ * signal can fire after the timer runs out but before the signal mask is
+ * restored. To do this, we fork a child process which installs a SIGINT
+ * handler and repeatedly calls either ppoll() or pselect() with a 1 ms
+ * timeout, while the parent repeatedly sends SIGINT to the child at
+ * intervals that start out at 1100 us and gradually decrease to 900 us.
+ * Each SIGINT resynchronizes parent and child, and sooner or later the
+ * parent hits the sweet spot and the SIGINT arrives at just the right
+ * time to demonstrate the bug.
+ */
+
+#include <sys/select.h>
+#include <sys/wait.h>
+
+#include <err.h>
+#include <errno.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static volatile sig_atomic_t caught[NSIG];
+
+static void
+handler(int signo)
+{
+ caught[signo]++;
+}
+
+static void
+child(int rd, bool poll)
+{
+ struct timespec timeout = { .tv_nsec = 1000000 };
+ sigset_t set0, set1;
+ int ret;
+
+ /* empty mask for ppoll() / pselect() */
+ sigemptyset(&set0);
+
+ /* block SIGINT, then install a handler for it */
+ sigemptyset(&set1);
+ sigaddset(&set1, SIGINT);
+ sigprocmask(SIG_BLOCK, &set1, NULL);
+ signal(SIGINT, handler);
+
+ /* signal parent that we are ready */
+ close(rd);
+ for (;;) {
+ /* sleep for 1 ms with signals unblocked */
+ ret = poll ? ppoll(NULL, 0, &timeout, &set0) :
+ pselect(0, NULL, NULL, NULL, &timeout, &set0);
+ /*
+ * At this point, either ret == 0 (timer ran out) errno ==
+ * EINTR (a signal was received). Any other outcome is
+ * abnormal.
+ */
+ if (ret != 0 && errno != EINTR)
+ err(1, "p%s()", poll ? "poll" : "select");
+ /* if ret == 0, we should not have caught any signals */
+ if (ret == 0 && caught[SIGINT]) {
+ /*
+ * We successfully demonstrated the race. Restore
+ * the default action and re-raise SIGINT.
+ */
+ signal(SIGINT, SIG_DFL);
+ raise(SIGINT);
+ /* Not reached */
+ }
+ /* reset for next attempt */
+ caught[SIGINT] = 0;
+ }
+ /* Not reached */
+}
+
+static void
+prace(bool poll)
+{
+ int pd[2], status;
+ pid_t pid;
+
+ /* fork child process */
+ if (pipe(pd) != 0)
+ err(1, "pipe()");
+ if ((pid = fork()) < 0)
+ err(1, "fork()");
+ if (pid == 0) {
+ close(pd[0]);
+ child(pd[1], poll);
+ /* Not reached */
+ }
+ close(pd[1]);
+
+ /* wait for child to signal readiness */
+ (void)read(pd[0], &pd[0], sizeof(pd[0]));
+ close(pd[0]);
+
+ /* repeatedly attempt to signal at just the right moment */
+ for (useconds_t timeout = 1100; timeout > 900; timeout--) {
+ usleep(timeout);
+ if (kill(pid, SIGINT) != 0) {
+ if (errno != ENOENT)
+ err(1, "kill()");
+ /* ENOENT means the child has terminated */
+ break;
+ }
+ }
+
+ /* we're done, kill the child for sure */
+ (void)kill(pid, SIGKILL);
+ if (waitpid(pid, &status, 0) < 0)
+ err(1, "waitpid()");
+
+ /* assert that the child died of SIGKILL */
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE_MSG(WTERMSIG(status) == SIGKILL,
+ "child caught SIG%s", sys_signame[WTERMSIG(status)]);
+}
+
+ATF_TC_WITHOUT_HEAD(ppoll_race);
+ATF_TC_BODY(ppoll_race, tc)
+{
+ prace(true);
+}
+
+ATF_TC_WITHOUT_HEAD(pselect_race);
+ATF_TC_BODY(pselect_race, tc)
+{
+ prace(false);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, ppoll_race);
+ ATF_TP_ADD_TC(tp, pselect_race);
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/ptrace_test.c b/tests/sys/kern/ptrace_test.c
index 675d90e144ae..d36dfe951e20 100644
--- a/tests/sys/kern/ptrace_test.c
+++ b/tests/sys/kern/ptrace_test.c
@@ -28,13 +28,13 @@
#include <sys/elf.h>
#include <sys/event.h>
#include <sys/file.h>
+#include <sys/mman.h>
#include <sys/time.h>
#include <sys/procctl.h>
#include <sys/procdesc.h>
#include <sys/ptrace.h>
#include <sys/procfs.h>
#include <sys/queue.h>
-#include <sys/runq.h>
#include <sys/syscall.h>
#include <sys/sysctl.h>
#include <sys/user.h>
@@ -2027,7 +2027,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_signal, tc)
sched_get_priority_min(SCHED_FIFO)) / 2;
CHILD_REQUIRE(pthread_setschedparam(pthread_self(),
SCHED_FIFO, &sched_param) == 0);
- sched_param.sched_priority -= RQ_PPQ;
+ sched_param.sched_priority -= 1;
CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO,
&sched_param) == 0);
@@ -2130,7 +2130,7 @@ ATF_TC_BODY(ptrace__PT_KILL_competing_stop, tc)
sched_get_priority_min(SCHED_FIFO)) / 2;
CHILD_REQUIRE(pthread_setschedparam(pthread_self(),
SCHED_FIFO, &sched_param) == 0);
- sched_param.sched_priority -= RQ_PPQ;
+ sched_param.sched_priority -= 1;
CHILD_REQUIRE(pthread_setschedparam(t, SCHED_FIFO,
&sched_param) == 0);
@@ -4161,6 +4161,53 @@ ATF_TC_BODY(ptrace__syscall_args, tc)
}
/*
+ * Check that syscall info is available whenever kernel has valid td_sa.
+ * Assumes that libc nanosleep(2) is the plain syscall wrapper.
+ */
+ATF_TC_WITHOUT_HEAD(ptrace__syscall_args_anywhere);
+ATF_TC_BODY(ptrace__syscall_args_anywhere, tc)
+{
+ struct timespec rqt;
+ struct ptrace_lwpinfo lwpi;
+ register_t args[8];
+ pid_t debuggee, wpid;
+ int error, status;
+
+ debuggee = fork();
+ ATF_REQUIRE(debuggee >= 0);
+ if (debuggee == 0) {
+ rqt.tv_sec = 100000;
+ rqt.tv_nsec = 0;
+ for (;;)
+ nanosleep(&rqt, NULL);
+ _exit(0);
+ }
+
+ /* Give the debuggee some time to go to sleep. */
+ sleep(2);
+ error = ptrace(PT_ATTACH, debuggee, 0, 0);
+ ATF_REQUIRE(error == 0);
+ wpid = waitpid(debuggee, &status, 0);
+ REQUIRE_EQ(wpid, debuggee);
+ ATF_REQUIRE(WIFSTOPPED(status));
+ REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+
+ error = ptrace(PT_LWPINFO, debuggee, (caddr_t)&lwpi, sizeof(lwpi));
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE(lwpi.pl_syscall_code == SYS_nanosleep);
+ ATF_REQUIRE(lwpi.pl_syscall_narg == 2);
+ error = ptrace(PT_GET_SC_ARGS, debuggee, (caddr_t)&args[0],
+ lwpi.pl_syscall_narg * sizeof(register_t));
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE(args[0] == (register_t)&rqt);
+ ATF_REQUIRE(args[1] == 0);
+
+ error = ptrace(PT_DETACH, debuggee, 0, 0);
+ ATF_REQUIRE(error == 0);
+ kill(SIGKILL, debuggee);
+}
+
+/*
* Verify that when the process is traced that it isn't reparent
* to the init process when we close all process descriptors.
*/
@@ -4331,7 +4378,10 @@ ATF_TC_BODY(ptrace__PT_SC_REMOTE_getpid, tc)
exit(0);
}
- attach_child(fpid);
+ wpid = waitpid(fpid, &status, 0);
+ REQUIRE_EQ(wpid, fpid);
+ ATF_REQUIRE(WIFSTOPPED(status));
+ REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
pscr.pscr_syscall = SYS_getpid;
pscr.pscr_nargs = 0;
@@ -4414,6 +4464,132 @@ ATF_TC_BODY(ptrace__reap_kill_stopped, tc)
REQUIRE_EQ(-1, prk.rk_fpid);
}
+struct child_res {
+ struct timespec sleep_time;
+ int nanosleep_res;
+ int nanosleep_errno;
+};
+
+static const long nsec = 1000000000L;
+static const struct timespec ten_sec = {
+ .tv_sec = 10,
+ .tv_nsec = 0,
+};
+static const struct timespec twelve_sec = {
+ .tv_sec = 12,
+ .tv_nsec = 0,
+};
+
+ATF_TC_WITHOUT_HEAD(ptrace__PT_ATTACH_no_EINTR);
+ATF_TC_BODY(ptrace__PT_ATTACH_no_EINTR, tc)
+{
+ struct child_res *shm;
+ struct timespec rqt, now, wake;
+ pid_t debuggee;
+ int status;
+
+ shm = mmap(NULL, sizeof(*shm), PROT_READ | PROT_WRITE,
+ MAP_SHARED | MAP_ANON, -1, 0);
+ ATF_REQUIRE(shm != MAP_FAILED);
+
+ ATF_REQUIRE((debuggee = fork()) != -1);
+ if (debuggee == 0) {
+ rqt.tv_sec = 10;
+ rqt.tv_nsec = 0;
+ clock_gettime(CLOCK_MONOTONIC_PRECISE, &now);
+ errno = 0;
+ shm->nanosleep_res = nanosleep(&rqt, NULL);
+ shm->nanosleep_errno = errno;
+ clock_gettime(CLOCK_MONOTONIC_PRECISE, &wake);
+ timespecsub(&wake, &now, &shm->sleep_time);
+ _exit(0);
+ }
+
+ /* Give the debuggee some time to go to sleep. */
+ sleep(2);
+ REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0);
+ REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
+ ATF_REQUIRE(WIFSTOPPED(status));
+ REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+
+ REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0);
+ REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
+ ATF_REQUIRE(WIFEXITED(status));
+ REQUIRE_EQ(WEXITSTATUS(status), 0);
+
+ ATF_REQUIRE(shm->nanosleep_res == 0);
+ ATF_REQUIRE(shm->nanosleep_errno == 0);
+ ATF_REQUIRE(timespeccmp(&shm->sleep_time, &ten_sec, >=));
+ ATF_REQUIRE(timespeccmp(&shm->sleep_time, &twelve_sec, <=));
+}
+
+ATF_TC_WITHOUT_HEAD(ptrace__PT_DETACH_continued);
+ATF_TC_BODY(ptrace__PT_DETACH_continued, tc)
+{
+ char buf[256];
+ pid_t debuggee, debugger;
+ int dpipe[2] = {-1, -1}, status;
+
+ /* Setup the debuggee's pipe, which we'll use to let it terminate. */
+ ATF_REQUIRE(pipe(dpipe) == 0);
+ ATF_REQUIRE((debuggee = fork()) != -1);
+
+ if (debuggee == 0) {
+ ssize_t readsz;
+
+ /*
+ * The debuggee will just absorb everything until the parent
+ * closes it. In the process, we expect it to get SIGSTOP'd,
+ * then ptrace(2)d and finally, it should resume after we detach
+ * and the parent will be notified.
+ */
+ close(dpipe[1]);
+ while ((readsz = read(dpipe[0], buf, sizeof(buf))) != 0) {
+ if (readsz > 0 || errno == EINTR)
+ continue;
+ _exit(1);
+ }
+
+ _exit(0);
+ }
+
+ close(dpipe[0]);
+
+ ATF_REQUIRE(kill(debuggee, SIGSTOP) == 0);
+ REQUIRE_EQ(waitpid(debuggee, &status, WUNTRACED), debuggee);
+ ATF_REQUIRE(WIFSTOPPED(status));
+
+ /* Child is stopped, enter the debugger to attach/detach. */
+ ATF_REQUIRE((debugger = fork()) != -1);
+ if (debugger == 0) {
+ REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0);
+ REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
+ ATF_REQUIRE(WIFSTOPPED(status));
+ REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
+
+ REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0);
+ _exit(0);
+ }
+
+ REQUIRE_EQ(waitpid(debugger, &status, 0), debugger);
+ ATF_REQUIRE(WIFEXITED(status));
+ REQUIRE_EQ(WEXITSTATUS(status), 0);
+
+ REQUIRE_EQ(waitpid(debuggee, &status, WCONTINUED), debuggee);
+ ATF_REQUIRE(WIFCONTINUED(status));
+
+ /*
+ * Closing the pipe will trigger the debuggee to exit now that the
+ * child has resumed following detach.
+ */
+ close(dpipe[1]);
+
+ REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
+ ATF_REQUIRE(WIFEXITED(status));
+ REQUIRE_EQ(WEXITSTATUS(status), 0);
+
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_trace_me);
@@ -4476,11 +4652,14 @@ ATF_TP_ADD_TCS(tp)
#endif
ATF_TP_ADD_TC(tp, ptrace__PT_LWPINFO_stale_siginfo);
ATF_TP_ADD_TC(tp, ptrace__syscall_args);
+ ATF_TP_ADD_TC(tp, ptrace__syscall_args_anywhere);
ATF_TP_ADD_TC(tp, ptrace__proc_reparent);
ATF_TP_ADD_TC(tp, ptrace__procdesc_wait_child);
ATF_TP_ADD_TC(tp, ptrace__procdesc_reparent_wait_child);
ATF_TP_ADD_TC(tp, ptrace__PT_SC_REMOTE_getpid);
ATF_TP_ADD_TC(tp, ptrace__reap_kill_stopped);
+ ATF_TP_ADD_TC(tp, ptrace__PT_ATTACH_no_EINTR);
+ ATF_TP_ADD_TC(tp, ptrace__PT_DETACH_continued);
return (atf_no_error());
}
diff --git a/tests/sys/kern/sched_affinity.c b/tests/sys/kern/sched_affinity.c
index cbba226b3fa2..9af6076c1649 100644
--- a/tests/sys/kern/sched_affinity.c
+++ b/tests/sys/kern/sched_affinity.c
@@ -67,13 +67,16 @@ ATF_TC_BODY(test_setinvalidcpu, tc)
{
size_t cpusetsize;
cpuset_t *set;
+ int cpu;
+
+ cpu = maxcpuid > 1 ? maxcpuid - 1 : 0;
cpusetsize = CPU_ALLOC_SIZE(maxcpuid + 1);
set = CPU_ALLOC(maxcpuid + 1);
ATF_REQUIRE(set != NULL);
CPU_ZERO_S(cpusetsize, set);
CPU_SET_S(maxcpuid + 1, cpusetsize, set);
- CPU_SET_S(maxcpuid - 1, cpusetsize, set);
+ CPU_SET_S(cpu, cpusetsize, set);
ATF_REQUIRE(sched_setaffinity(0, cpusetsize, set) == 0);
CPU_FREE(set);
@@ -82,7 +85,7 @@ ATF_TC_BODY(test_setinvalidcpu, tc)
ATF_REQUIRE(set != NULL);
CPU_ZERO_S(cpusetsize, set);
CPU_SET_S(maxcpuid + 1, cpusetsize, set);
- CPU_SET_S(maxcpuid - 1, cpusetsize, set);
+ CPU_SET_S(cpu, cpusetsize, set);
ATF_REQUIRE(cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID,
-1, cpusetsize, set) == -1);
ATF_REQUIRE_EQ(errno, EINVAL);
diff --git a/tests/sys/kern/sendfile_helper.c b/tests/sys/kern/sendfile_helper.c
index 703b04fdea6c..6365531e312c 100644
--- a/tests/sys/kern/sendfile_helper.c
+++ b/tests/sys/kern/sendfile_helper.c
@@ -84,7 +84,7 @@ tcp_socketpair(int *sv)
if (fcntl(sv[0], F_SETFL, flags) == -1)
err(1, "fcntl +O_NONBLOCK");
- if (connect(sv[0], (void *)&sin, sizeof(sin)) != -1 ||
+ if (connect(sv[0], (void *)&sin, sizeof(sin)) == -1 &&
errno != EINPROGRESS)
err(1, "connect cs");
diff --git a/tests/sys/kern/socket_splice.c b/tests/sys/kern/socket_splice.c
new file mode 100644
index 000000000000..dfd4cb4f5957
--- /dev/null
+++ b/tests/sys/kern/socket_splice.c
@@ -0,0 +1,979 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 Stormshield
+ */
+
+#include <sys/capsicum.h>
+#include <sys/event.h>
+#include <sys/filio.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+
+#include <errno.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#include <atf-c.h>
+
+static void
+checked_close(int fd)
+{
+ int error;
+
+ error = close(fd);
+ ATF_REQUIRE_MSG(error == 0, "close failed: %s", strerror(errno));
+}
+
+static int
+fionread(int fd)
+{
+ int data, error;
+
+ data = 0;
+ error = ioctl(fd, FIONREAD, &data);
+ ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s", strerror(errno));
+ ATF_REQUIRE(data >= 0);
+ return (data);
+}
+
+static void
+noblocking(int fd)
+{
+ int flags, error;
+
+ flags = fcntl(fd, F_GETFL);
+ ATF_REQUIRE_MSG(flags != -1, "fcntl failed: %s", strerror(errno));
+ flags |= O_NONBLOCK;
+ error = fcntl(fd, F_SETFL, flags);
+ ATF_REQUIRE_MSG(error == 0, "fcntl failed: %s", strerror(errno));
+}
+
+/*
+ * Create a pair of connected TCP sockets, returned via the "out" array.
+ */
+static void
+tcp_socketpair(int out[2], int domain)
+{
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ struct sockaddr *sinp;
+ int error, sd[2];
+
+ sd[0] = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd[0] >= 0, "socket failed: %s", strerror(errno));
+ sd[1] = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd[1] >= 0, "socket failed: %s", strerror(errno));
+
+ error = setsockopt(sd[0], IPPROTO_TCP, TCP_NODELAY, &(int){ 1 },
+ sizeof(int));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+ error = setsockopt(sd[1], IPPROTO_TCP, TCP_NODELAY, &(int){ 1 },
+ sizeof(int));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ if (domain == PF_INET) {
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ sin.sin_port = htons(0);
+ sinp = (struct sockaddr *)&sin;
+ } else {
+ ATF_REQUIRE(domain == PF_INET6);
+ memset(&sin6, 0, sizeof(sin6));
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_len = sizeof(sin6);
+ sin6.sin6_addr = in6addr_loopback;
+ sin6.sin6_port = htons(0);
+ sinp = (struct sockaddr *)&sin6;
+ }
+
+ error = bind(sd[0], sinp, sinp->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(sd[0], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ error = getsockname(sd[0], sinp, &(socklen_t){ sinp->sa_len });
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ error = connect(sd[1], sinp, sinp->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno));
+ out[0] = accept(sd[0], NULL, NULL);
+ ATF_REQUIRE_MSG(out[0] >= 0, "accept failed: %s", strerror(errno));
+ checked_close(sd[0]);
+ out[1] = sd[1];
+}
+
+static void
+tcp4_socketpair(int out[2])
+{
+ tcp_socketpair(out, PF_INET);
+}
+
+static void
+tcp6_socketpair(int out[2])
+{
+ tcp_socketpair(out, PF_INET6);
+}
+
+static off_t
+nspliced(int sd)
+{
+ off_t n;
+ socklen_t len;
+ int error;
+
+ len = sizeof(n);
+ error = getsockopt(sd, SOL_SOCKET, SO_SPLICE, &n, &len);
+ ATF_REQUIRE_MSG(error == 0, "getsockopt failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(len == sizeof(n), "unexpected length: %d", len);
+ return (n);
+}
+
+/*
+ * Use a macro so that ATF_REQUIRE_MSG prints a useful line number.
+ */
+#define check_nspliced(sd, n) do { \
+ off_t sofar; \
+ \
+ sofar = nspliced(sd); \
+ ATF_REQUIRE_MSG(sofar == (off_t)n, "spliced %jd bytes, expected %jd", \
+ (intmax_t)sofar, (intmax_t)n); \
+} while (0)
+
+static void
+splice_init(struct splice *sp, int fd, off_t max, struct timeval *tv)
+{
+ memset(sp, 0, sizeof(*sp));
+ sp->sp_fd = fd;
+ sp->sp_max = max;
+ if (tv != NULL)
+ sp->sp_idle = *tv;
+ else
+ sp->sp_idle.tv_sec = sp->sp_idle.tv_usec = 0;
+}
+
+static void
+unsplice(int fd)
+{
+ struct splice sp;
+ int error;
+
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(fd, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+}
+
+static void
+unsplice_pair(int fd1, int fd2)
+{
+ unsplice(fd1);
+ unsplice(fd2);
+}
+
+static void
+splice_pair(int fd1, int fd2, off_t max, struct timeval *tv)
+{
+ struct splice sp;
+ int error;
+
+ splice_init(&sp, fd1, max, tv);
+ error = setsockopt(fd2, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ splice_init(&sp, fd2, max, tv);
+ error = setsockopt(fd1, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+}
+
+/*
+ * A structure representing a spliced pair of connections. left[1] is
+ * bidirectionally spliced with right[0].
+ */
+struct splice_conn {
+ int left[2];
+ int right[2];
+};
+
+/*
+ * Initialize a splice connection with the given maximum number of bytes to
+ * splice and the given idle timeout. For now we're forced to use TCP socket,
+ * but at some point it would be nice (and simpler) to use pairs of PF_LOCAL
+ * sockets.
+ */
+static void
+splice_conn_init_limits(struct splice_conn *sc, off_t max, struct timeval *tv)
+{
+ memset(sc, 0, sizeof(*sc));
+ tcp4_socketpair(sc->left);
+ tcp4_socketpair(sc->right);
+ splice_pair(sc->left[1], sc->right[0], max, tv);
+}
+
+static void
+splice_conn_init(struct splice_conn *sc)
+{
+ splice_conn_init_limits(sc, 0, NULL);
+}
+
+static void
+splice_conn_check_empty(struct splice_conn *sc)
+{
+ int data;
+
+ data = fionread(sc->left[0]);
+ ATF_REQUIRE_MSG(data == 0, "unexpected data on left[0]: %d", data);
+ data = fionread(sc->left[1]);
+ ATF_REQUIRE_MSG(data == 0, "unexpected data on left[1]: %d", data);
+ data = fionread(sc->right[0]);
+ ATF_REQUIRE_MSG(data == 0, "unexpected data on right[0]: %d", data);
+ data = fionread(sc->right[1]);
+ ATF_REQUIRE_MSG(data == 0, "unexpected data on right[1]: %d", data);
+}
+
+static void
+splice_conn_fini(struct splice_conn *sc)
+{
+ checked_close(sc->left[0]);
+ checked_close(sc->left[1]);
+ checked_close(sc->right[0]);
+ checked_close(sc->right[1]);
+}
+
+static void
+splice_conn_noblocking(struct splice_conn *sc)
+{
+ noblocking(sc->left[0]);
+ noblocking(sc->left[1]);
+ noblocking(sc->right[0]);
+ noblocking(sc->right[1]);
+}
+
+/* Pass a byte through a pair of spliced connections. */
+ATF_TC_WITHOUT_HEAD(splice_basic);
+ATF_TC_BODY(splice_basic, tc)
+{
+ struct splice_conn sc;
+ ssize_t n;
+ char c;
+
+ splice_conn_init(&sc);
+
+ check_nspliced(sc.left[1], 0);
+ check_nspliced(sc.right[0], 0);
+
+ /* Left-to-right. */
+ c = 'M';
+ n = write(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 0);
+
+ /* Right-to-left. */
+ c = 'J';
+ n = write(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 1);
+
+ /* Unsplice and verify that the byte counts haven't changed. */
+ unsplice(sc.left[1]);
+ unsplice(sc.right[0]);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 1);
+
+ splice_conn_fini(&sc);
+}
+
+static void
+remove_rights(int fd, const cap_rights_t *toremove)
+{
+ cap_rights_t rights;
+ int error;
+
+ error = cap_rights_get(fd, &rights);
+ ATF_REQUIRE_MSG(error == 0, "cap_rights_get failed: %s",
+ strerror(errno));
+ cap_rights_remove(&rights, toremove);
+ error = cap_rights_limit(fd, &rights);
+ ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+ strerror(errno));
+}
+
+/*
+ * Verify that splicing fails when the socket is missing the necessary rights.
+ */
+ATF_TC_WITHOUT_HEAD(splice_capsicum);
+ATF_TC_BODY(splice_capsicum, tc)
+{
+ struct splice sp;
+ cap_rights_t rights;
+ off_t n;
+ int error, left[2], right[2];
+
+ tcp4_socketpair(left);
+ tcp4_socketpair(right);
+
+ /*
+ * Make sure that we can't splice a socket that's missing recv rights.
+ */
+ remove_rights(left[1], cap_rights_init(&rights, CAP_RECV));
+ splice_init(&sp, right[0], 0, NULL);
+ error = setsockopt(left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, error == -1);
+
+ /* Make sure we can still splice left[1] in the other direction. */
+ splice_init(&sp, left[1], 0, NULL);
+ error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ /*
+ * Now remove send rights from left[1] and verify that splicing is no
+ * longer possible.
+ */
+ remove_rights(left[1], cap_rights_init(&rights, CAP_SEND));
+ splice_init(&sp, left[1], 0, NULL);
+ error = setsockopt(right[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCAPABLE, error == -1);
+
+ /*
+ * It's still ok to query the SO_SPLICE state though.
+ */
+ n = -1;
+ error = getsockopt(left[1], SOL_SOCKET, SO_SPLICE, &n,
+ &(socklen_t){ sizeof(n) });
+ ATF_REQUIRE_MSG(error == 0, "getsockopt failed: %s", strerror(errno));
+ ATF_REQUIRE(n == 0);
+
+ /*
+ * Make sure that we can unsplice a spliced pair without any rights
+ * other than CAP_SETSOCKOPT.
+ */
+ splice_pair(left[0], right[1], 0, NULL);
+ error = cap_rights_limit(left[0],
+ cap_rights_init(&rights, CAP_SETSOCKOPT));
+ ATF_REQUIRE_MSG(error == 0, "cap_rights_limit failed: %s",
+ strerror(errno));
+ unsplice(left[0]);
+
+ checked_close(left[0]);
+ checked_close(left[1]);
+ checked_close(right[0]);
+ checked_close(right[1]);
+}
+
+/*
+ * Check various error cases in splice configuration.
+ */
+ATF_TC_WITHOUT_HEAD(splice_error);
+ATF_TC_BODY(splice_error, tc)
+{
+ struct splice_conn sc;
+ struct splice sp;
+ char path[PATH_MAX];
+ int error, fd, sd, usd[2];
+
+ memset(&sc, 0, sizeof(sc));
+ tcp4_socketpair(sc.left);
+ tcp4_socketpair(sc.right);
+
+ /* A negative byte limit is invalid. */
+ splice_init(&sp, sc.right[0], -3, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+
+ /* Can't unsplice a never-spliced socket. */
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCONN, error == -1);
+
+ /* Can't double-unsplice a socket. */
+ splice_init(&sp, sc.right[0], 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+ unsplice(sc.left[1]);
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCONN, error == -1);
+
+ /* Can't splice a spliced socket */
+ splice_init(&sp, sc.right[0], 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+ splice_init(&sp, sc.right[1], 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EBUSY, error == -1);
+ splice_init(&sp, sc.right[0], 0, NULL);
+ error = setsockopt(sc.left[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EBUSY, error == -1);
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+
+ /* Can't splice to a non-socket. */
+ snprintf(path, sizeof(path), "/tmp/splice_error.XXXXXX");
+ fd = mkstemp(path);
+ ATF_REQUIRE_MSG(fd >= 0, "mkstemp failed: %s", strerror(errno));
+ splice_init(&sp, fd, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTSOCK, error == -1);
+
+ /* Can't splice to an invalid fd. */
+ checked_close(fd);
+ splice_init(&sp, fd, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EBADF, error == -1);
+
+ /* Can't splice a unix stream socket. */
+ error = socketpair(AF_UNIX, SOCK_STREAM, 0, usd);
+ ATF_REQUIRE_MSG(error == 0, "socketpair failed: %s", strerror(errno));
+ splice_init(&sp, usd[0], 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EPROTONOSUPPORT, error == -1);
+ error = setsockopt(usd[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EPROTONOSUPPORT, error == -1);
+ checked_close(usd[0]);
+ checked_close(usd[1]);
+
+ /* Can't splice an unconnected TCP socket. */
+ sd = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd >= 0, "socket failed: %s", strerror(errno));
+ splice_init(&sp, sd, 0, NULL);
+ error = setsockopt(sc.left[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCONN, error == -1);
+ splice_init(&sp, sc.right[0], 0, NULL);
+ error = setsockopt(sd, SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(ENOTCONN, error == -1);
+
+ splice_conn_fini(&sc);
+}
+
+/*
+ * Make sure that kevent() doesn't report read I/O events on spliced sockets.
+ */
+ATF_TC_WITHOUT_HEAD(splice_kevent);
+ATF_TC_BODY(splice_kevent, tc)
+{
+ struct splice_conn sc;
+ struct kevent kev;
+ struct timespec ts;
+ ssize_t n;
+ int error, nev, kq;
+ uint8_t b;
+
+ splice_conn_init(&sc);
+
+ kq = kqueue();
+ ATF_REQUIRE_MSG(kq >= 0, "kqueue failed: %s", strerror(errno));
+
+ EV_SET(&kev, sc.left[1], EVFILT_READ, EV_ADD, 0, 0, NULL);
+ error = kevent(kq, &kev, 1, NULL, 0, NULL);
+ ATF_REQUIRE_MSG(error == 0, "kevent failed: %s", strerror(errno));
+
+ memset(&ts, 0, sizeof(ts));
+ nev = kevent(kq, NULL, 0, &kev, 1, &ts);
+ ATF_REQUIRE_MSG(nev >= 0, "kevent failed: %s", strerror(errno));
+ ATF_REQUIRE(nev == 0);
+
+ b = 'M';
+ n = write(sc.left[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.right[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'M');
+
+ nev = kevent(kq, NULL, 0, &kev, 1, &ts);
+ ATF_REQUIRE_MSG(nev >= 0, "kevent failed: %s", strerror(errno));
+ ATF_REQUIRE(nev == 0);
+
+ b = 'J';
+ n = write(sc.right[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'J');
+
+ splice_conn_fini(&sc);
+ checked_close(kq);
+}
+
+/*
+ * Verify that a splice byte limit is applied.
+ */
+ATF_TC_WITHOUT_HEAD(splice_limit_bytes);
+ATF_TC_BODY(splice_limit_bytes, tc)
+{
+ struct splice_conn sc;
+ ssize_t n;
+ uint8_t b, buf[128];
+
+ splice_conn_init_limits(&sc, sizeof(buf) + 1, NULL);
+
+ memset(buf, 'A', sizeof(buf));
+ for (size_t total = sizeof(buf); total > 0; total -= n) {
+ n = write(sc.left[0], buf, total);
+ ATF_REQUIRE_MSG(n > 0, "write failed: %s", strerror(errno));
+ }
+ for (size_t total = sizeof(buf); total > 0; total -= n) {
+ n = read(sc.right[1], buf, sizeof(buf));
+ ATF_REQUIRE_MSG(n > 0, "read failed: %s", strerror(errno));
+ }
+
+ check_nspliced(sc.left[1], sizeof(buf));
+ check_nspliced(sc.right[0], 0);
+
+ /* Trigger an unsplice by writing the last byte. */
+ b = 'B';
+ n = write(sc.left[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.right[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'B');
+
+ /*
+ * The next byte should appear on the other side of the connection
+ * rather than the splice.
+ */
+ b = 'C';
+ n = write(sc.left[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'C');
+
+ splice_conn_check_empty(&sc);
+
+ splice_conn_fini(&sc);
+}
+
+/*
+ * Verify that a splice timeout limit is applied.
+ */
+ATF_TC_WITHOUT_HEAD(splice_limit_timeout);
+ATF_TC_BODY(splice_limit_timeout, tc)
+{
+ struct splice_conn sc;
+ ssize_t n;
+ int error;
+ uint8_t b, buf[128];
+
+ splice_conn_init_limits(&sc, 0,
+ &(struct timeval){ .tv_sec = 0, .tv_usec = 500000 /* 500ms */ });
+
+ /* Write some data through the splice. */
+ memset(buf, 'A', sizeof(buf));
+ for (size_t total = sizeof(buf); total > 0; total -= n) {
+ n = write(sc.left[0], buf, total);
+ ATF_REQUIRE_MSG(n > 0, "write failed: %s", strerror(errno));
+ }
+ for (size_t total = sizeof(buf); total > 0; total -= n) {
+ n = read(sc.right[1], buf, sizeof(buf));
+ ATF_REQUIRE_MSG(n > 0, "read failed: %s", strerror(errno));
+ }
+
+ check_nspliced(sc.left[1], sizeof(buf));
+ check_nspliced(sc.right[0], 0);
+
+ /* Wait for the splice to time out. */
+ error = usleep(550000);
+ ATF_REQUIRE_MSG(error == 0, "usleep failed: %s", strerror(errno));
+
+ /*
+ * The next byte should appear on the other side of the connection
+ * rather than the splice.
+ */
+ b = 'C';
+ n = write(sc.left[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'C');
+
+ splice_conn_fini(&sc);
+}
+
+/*
+ * Make sure that listen() fails on spliced sockets, and that SO_SPLICE can't be
+ * used with listening sockets.
+ */
+ATF_TC_WITHOUT_HEAD(splice_listen);
+ATF_TC_BODY(splice_listen, tc)
+{
+ struct splice sp;
+ struct splice_conn sc;
+ int error, sd[3];
+
+ /*
+ * These should fail regardless since the sockets are connected, but it
+ * doesn't hurt to check.
+ */
+ splice_conn_init(&sc);
+ error = listen(sc.left[1], 1);
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+ error = listen(sc.right[0], 1);
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+ splice_conn_fini(&sc);
+
+ tcp4_socketpair(sd);
+ sd[2] = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd[2] >= 0, "socket failed: %s", strerror(errno));
+ error = listen(sd[2], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ /*
+ * Make sure a listening socket can't be spliced in either direction.
+ */
+ splice_init(&sp, sd[2], 0, NULL);
+ error = setsockopt(sd[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+ splice_init(&sp, sd[1], 0, NULL);
+ error = setsockopt(sd[2], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+
+ /*
+ * Make sure we can't try to unsplice a listening socket.
+ */
+ splice_init(&sp, -1, 0, NULL);
+ error = setsockopt(sd[2], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+
+ checked_close(sd[0]);
+ checked_close(sd[1]);
+ checked_close(sd[2]);
+}
+
+static void
+sigalarm(int sig __unused)
+{
+}
+
+/*
+ * Our SO_SPLICE implementation doesn't do anything to prevent loops. We should
+ * however make sure that they are interruptible.
+ */
+ATF_TC_WITHOUT_HEAD(splice_loop);
+ATF_TC_BODY(splice_loop, tc)
+{
+ ssize_t n;
+ int sd[2], status;
+ pid_t child;
+ char c;
+
+ tcp_socketpair(sd, PF_INET);
+ splice_pair(sd[0], sd[1], 0, NULL);
+
+ /*
+ * Let the child process trigger an infinite loop. It should still be
+ * possible to kill the child with a signal, causing the connection to
+ * be dropped and ending the loop.
+ */
+ child = fork();
+ ATF_REQUIRE_MSG(child >= 0, "fork failed: %s", strerror(errno));
+ if (child == 0) {
+ alarm(2);
+ c = 42;
+ n = write(sd[0], &c, 1);
+ if (n != 1)
+ _exit(2);
+ c = 24;
+ n = write(sd[1], &c, 1);
+ if (n != 1)
+ _exit(3);
+
+ for (;;) {
+ /* Wait for SIGALARM. */
+ sleep(100);
+ }
+
+ _exit(0);
+ } else {
+ checked_close(sd[0]);
+ checked_close(sd[1]);
+
+ child = waitpid(child, &status, 0);
+ ATF_REQUIRE_MSG(child >= 0,
+ "waitpid failed: %s", strerror(errno));
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE(WTERMSIG(status) == SIGALRM);
+ }
+}
+
+/*
+ * Simple I/O test.
+ */
+ATF_TC_WITHOUT_HEAD(splice_nonblock);
+ATF_TC_BODY(splice_nonblock, tc)
+{
+ struct splice_conn sc;
+ char buf[200];
+ size_t sofar;
+ ssize_t n;
+
+ splice_conn_init(&sc);
+ splice_conn_noblocking(&sc);
+
+ memset(buf, 'A', sizeof(buf));
+ for (sofar = 0;;) {
+ n = write(sc.left[0], buf, sizeof(buf));
+ if (n < 0) {
+ ATF_REQUIRE_ERRNO(EAGAIN, n == -1);
+ break;
+ }
+ sofar += n;
+ }
+
+ while (sofar > 0) {
+ n = read(sc.right[1], buf, sizeof(buf));
+ if (n < 0) {
+ ATF_REQUIRE_ERRNO(EAGAIN, n == -1);
+ usleep(100);
+ } else {
+ for (size_t i = 0; i < (size_t)n; i++)
+ ATF_REQUIRE(buf[i] == 'A');
+ sofar -= n;
+ }
+ }
+
+ splice_conn_fini(&sc);
+}
+
+ATF_TC_WITHOUT_HEAD(splice_resplice);
+ATF_TC_BODY(splice_resplice, tc)
+{
+ struct splice_conn sc;
+ ssize_t n;
+ char c;
+
+ splice_conn_init(&sc);
+
+ /* Left-to-right. */
+ c = 'M';
+ n = write(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 0);
+
+ /* Right-to-left. */
+ c = 'J';
+ n = write(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 1);
+
+ /* Unsplice and verify that the byte counts haven't changed. */
+ unsplice(sc.left[1]);
+ unsplice(sc.right[0]);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 1);
+
+ /* Splice again, check that byte counts are reset. */
+ splice_pair(sc.left[1], sc.right[0], 0, NULL);
+ check_nspliced(sc.left[1], 0);
+ check_nspliced(sc.right[0], 0);
+
+ /* Left-to-right. */
+ c = 'M';
+ n = write(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'M', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 0);
+
+ /* Right-to-left. */
+ c = 'J';
+ n = write(sc.right[1], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sc.left[0], &c, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(c == 'J', "unexpected character: %c", c);
+ check_nspliced(sc.left[1], 1);
+ check_nspliced(sc.right[0], 1);
+
+ splice_conn_fini(&sc);
+}
+
+struct xfer_args {
+ pthread_barrier_t *barrier;
+ uint32_t bytes;
+ int fd;
+};
+
+static void *
+xfer(void *arg)
+{
+ struct xfer_args *xfer;
+ uint8_t *buf;
+ size_t sz;
+ ssize_t n;
+ uint32_t resid;
+ int error;
+
+ xfer = arg;
+
+ error = fcntl(xfer->fd, F_SETFL, O_NONBLOCK);
+ ATF_REQUIRE_MSG(error == 0, "fcntl failed: %s", strerror(errno));
+
+ sz = MIN(xfer->bytes, 1024 * 1024);
+ buf = malloc(sz);
+ ATF_REQUIRE(buf != NULL);
+ arc4random_buf(buf, sz);
+
+ pthread_barrier_wait(xfer->barrier);
+
+ for (resid = xfer->bytes; xfer->bytes > 0 || resid > 0;) {
+ n = write(xfer->fd, buf, MIN(sz, xfer->bytes));
+ if (n < 0) {
+ ATF_REQUIRE_ERRNO(EAGAIN, n == -1);
+ usleep(1000);
+ } else {
+ ATF_REQUIRE(xfer->bytes >= (size_t)n);
+ xfer->bytes -= n;
+ }
+
+ n = read(xfer->fd, buf, sz);
+ if (n < 0) {
+ ATF_REQUIRE_ERRNO(EAGAIN, n == -1);
+ usleep(1000);
+ } else {
+ ATF_REQUIRE(resid >= (size_t)n);
+ resid -= n;
+ }
+ }
+
+ free(buf);
+ return (NULL);
+}
+
+/*
+ * Use two threads to transfer data between two spliced connections.
+ */
+ATF_TC_WITHOUT_HEAD(splice_throughput);
+ATF_TC_BODY(splice_throughput, tc)
+{
+ struct xfer_args xfers[2];
+ pthread_t thread[2];
+ pthread_barrier_t barrier;
+ struct splice_conn sc;
+ uint32_t bytes;
+ int error;
+
+ /* Transfer an amount between 1B and 1GB. */
+ bytes = arc4random_uniform(1024 * 1024 * 1024) + 1;
+ splice_conn_init(&sc);
+
+ error = pthread_barrier_init(&barrier, NULL, 2);
+ ATF_REQUIRE(error == 0);
+ xfers[0] = (struct xfer_args){
+ .barrier = &barrier,
+ .bytes = bytes,
+ .fd = sc.left[0]
+ };
+ xfers[1] = (struct xfer_args){
+ .barrier = &barrier,
+ .bytes = bytes,
+ .fd = sc.right[1]
+ };
+
+ error = pthread_create(&thread[0], NULL, xfer, &xfers[0]);
+ ATF_REQUIRE_MSG(error == 0,
+ "pthread_create failed: %s", strerror(errno));
+ error = pthread_create(&thread[1], NULL, xfer, &xfers[1]);
+ ATF_REQUIRE_MSG(error == 0,
+ "pthread_create failed: %s", strerror(errno));
+
+ error = pthread_join(thread[0], NULL);
+ ATF_REQUIRE_MSG(error == 0,
+ "pthread_join failed: %s", strerror(errno));
+ error = pthread_join(thread[1], NULL);
+ ATF_REQUIRE_MSG(error == 0,
+ "pthread_join failed: %s", strerror(errno));
+
+ error = pthread_barrier_destroy(&barrier);
+ ATF_REQUIRE(error == 0);
+ splice_conn_fini(&sc);
+}
+
+/*
+ * Make sure it's possible to splice v4 and v6 sockets together.
+ */
+ATF_TC_WITHOUT_HEAD(splice_v4v6);
+ATF_TC_BODY(splice_v4v6, tc)
+{
+ struct splice sp;
+ ssize_t n;
+ int sd4[2], sd6[2];
+ int error;
+ uint8_t b;
+
+ tcp4_socketpair(sd4);
+ tcp6_socketpair(sd6);
+
+ splice_init(&sp, sd6[0], 0, NULL);
+ error = setsockopt(sd4[1], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ splice_init(&sp, sd4[1], 0, NULL);
+ error = setsockopt(sd6[0], SOL_SOCKET, SO_SPLICE, &sp, sizeof(sp));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ b = 'M';
+ n = write(sd4[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sd6[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'M');
+
+ b = 'J';
+ n = write(sd6[1], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "write failed: %s", strerror(errno));
+ n = read(sd4[0], &b, 1);
+ ATF_REQUIRE_MSG(n == 1, "read failed: %s", strerror(errno));
+ ATF_REQUIRE(b == 'J');
+
+ checked_close(sd4[0]);
+ checked_close(sd4[1]);
+ checked_close(sd6[0]);
+ checked_close(sd6[1]);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, splice_basic);
+ ATF_TP_ADD_TC(tp, splice_capsicum);
+ ATF_TP_ADD_TC(tp, splice_error);
+ ATF_TP_ADD_TC(tp, splice_kevent);
+ ATF_TP_ADD_TC(tp, splice_limit_bytes);
+ ATF_TP_ADD_TC(tp, splice_limit_timeout);
+ ATF_TP_ADD_TC(tp, splice_listen);
+ ATF_TP_ADD_TC(tp, splice_loop);
+ ATF_TP_ADD_TC(tp, splice_nonblock);
+ ATF_TP_ADD_TC(tp, splice_resplice);
+ ATF_TP_ADD_TC(tp, splice_throughput);
+ ATF_TP_ADD_TC(tp, splice_v4v6);
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/sysctl_security_jail_children.sh b/tests/sys/kern/sysctl_security_jail_children.sh
new file mode 100644
index 000000000000..ac990e57ff74
--- /dev/null
+++ b/tests/sys/kern/sysctl_security_jail_children.sh
@@ -0,0 +1,77 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+atf_test_case "max_cur" "cleanup"
+max_cur_head()
+{
+ atf_set descr 'Test maximum and current number of child jails'
+ atf_set require.user root
+ atf_set execenv jail
+}
+max_cur_body()
+{
+ origin_max=$(sysctl -n security.jail.children.max)
+ origin_cur=$(sysctl -n security.jail.children.cur)
+
+ # Magic numbers reasoning:
+ # 3 stands for:
+ # - the test creates three jails: childfree, maxallowed, maxallowed.family
+ # 6 stands for:
+ # - maxallowed.family wants to set children.max=4
+ # - it means that its parent (maxallowed) should have at least children.max=5
+ # - it makes the origin (parent of maxallowed) provide children.max=6 minimum
+ #
+ test $origin_cur -le $origin_max || atf_fail "Abnormal cur=$origin_cur > max=$origin_max."
+ test $((origin_max - origin_cur)) -ge 3 || atf_skip "Not enough child jails are allowed for the test."
+ test $origin_max -ge 6 || atf_skip "Not high enough children.max limit for the test."
+
+ jail -c name=childfree persist
+ atf_check_equal "$((origin_cur + 1))" "$(sysctl -n security.jail.children.cur)"
+ atf_check_equal "0" "$(jexec childfree sysctl -n security.jail.children.max)"
+ atf_check_equal "0" "$(jexec childfree sysctl -n security.jail.children.cur)"
+
+ jail -c name=maxallowed children.max=$((origin_max - 1)) persist
+ atf_check_equal "$((origin_cur + 2))" "$(sysctl -n security.jail.children.cur)"
+ atf_check_equal "$((origin_max - 1))" "$(jexec maxallowed sysctl -n security.jail.children.max)"
+ atf_check_equal "0" "$(jexec maxallowed sysctl -n security.jail.children.cur)"
+
+ jexec maxallowed jail -c name=family children.max=4 persist
+ atf_check_equal "$((origin_cur + 3))" "$(sysctl -n security.jail.children.cur)"
+ atf_check_equal "1" "$(jexec maxallowed sysctl -n security.jail.children.cur)"
+ atf_check_equal "4" "$(jexec maxallowed.family sysctl -n security.jail.children.max)"
+ atf_check_equal "0" "$(jexec maxallowed.family sysctl -n security.jail.children.cur)"
+}
+max_cur_cleanup()
+{
+ jail -r maxallowed
+ jail -r childfree
+ return 0
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "max_cur"
+}
diff --git a/tests/sys/kern/tty/Makefile b/tests/sys/kern/tty/Makefile
new file mode 100644
index 000000000000..8628ab79875f
--- /dev/null
+++ b/tests/sys/kern/tty/Makefile
@@ -0,0 +1,15 @@
+TESTSDIR= ${TESTSBASE}/sys/kern/tty
+BINDIR= ${TESTSDIR}
+
+PLAIN_TESTS_PORCH+= test_canon
+PLAIN_TESTS_PORCH+= test_canon_fullbuf
+PLAIN_TESTS_PORCH+= test_ncanon
+PLAIN_TESTS_PORCH+= test_recanon
+ATF_TESTS_C+= test_sti
+
+PROGS+= fionread
+PROGS+= readsz
+
+LIBADD.test_sti= util
+
+.include <bsd.test.mk>
diff --git a/tests/sys/kern/tty/fionread.c b/tests/sys/kern/tty/fionread.c
new file mode 100644
index 000000000000..929d613f883b
--- /dev/null
+++ b/tests/sys/kern/tty/fionread.c
@@ -0,0 +1,21 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/ioctl.h>
+
+#include <assert.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int
+main(void)
+{
+ int nb;
+
+ assert(ioctl(STDIN_FILENO, FIONREAD, &nb) == 0);
+ printf("%d", nb);
+ return (0);
+}
diff --git a/tests/sys/kern/tty/readsz.c b/tests/sys/kern/tty/readsz.c
new file mode 100644
index 000000000000..95dafa02472f
--- /dev/null
+++ b/tests/sys/kern/tty/readsz.c
@@ -0,0 +1,130 @@
+/*-
+ * Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static void
+usage(void)
+{
+
+ fprintf(stderr, "usage: %s [-b bytes | -c lines | -e] [-s buffer-size]\n",
+ getprogname());
+ exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char *buf;
+ const char *errstr;
+ size_t bufsz = 0, reps;
+ ssize_t ret;
+ enum { MODE_BYTES, MODE_COUNT, MODE_EOF } mode;
+ int ch;
+
+ /*
+ * -b specifies number of bytes.
+ * -c specifies number of read() calls.
+ * -e specifies eof (default)
+ * -s to pass a buffer size
+ *
+ * Reading N lines is the same as -c with a high buffer size.
+ */
+ mode = MODE_EOF;
+ while ((ch = getopt(argc, argv, "b:c:es:")) != -1) {
+ switch (ch) {
+ case 'b':
+ mode = MODE_BYTES;
+ reps = strtonum(optarg, 0, SSIZE_MAX, &errstr);
+ if (errstr != NULL)
+ errx(1, "strtonum: %s", errstr);
+ break;
+ case 'c':
+ mode = MODE_COUNT;
+ reps = strtonum(optarg, 1, SSIZE_MAX, &errstr);
+ if (errstr != NULL)
+ errx(1, "strtonum: %s", errstr);
+ break;
+ case 'e':
+ mode = MODE_EOF;
+ break;
+ case 's':
+ bufsz = strtonum(optarg, 1, SSIZE_MAX, &errstr);
+ if (errstr != NULL)
+ errx(1, "strtonum: %s", errstr);
+ break;
+ default:
+ usage();
+ }
+ }
+
+ if (bufsz == 0) {
+ if (mode == MODE_BYTES)
+ bufsz = reps;
+ else
+ bufsz = LINE_MAX;
+ }
+
+ buf = malloc(bufsz);
+ if (buf == NULL)
+ err(1, "malloc");
+
+ for (;;) {
+ size_t readsz;
+
+ /*
+ * Be careful not to over-read if we're in byte-mode. In every other
+ * mode, we'll read as much as we can.
+ */
+ if (mode == MODE_BYTES)
+ readsz = MIN(bufsz, reps);
+ else
+ readsz = bufsz;
+
+ ret = read(STDIN_FILENO, buf, readsz);
+ if (ret == -1 && errno == EINTR)
+ continue;
+ if (ret == -1)
+ err(1, "read");
+ if (ret == 0) {
+ if (mode == MODE_EOF)
+ return (0);
+ errx(1, "premature EOF");
+ }
+
+ /* Write out what we've got */
+ write(STDOUT_FILENO, buf, ret);
+
+ /*
+ * Bail out if we've hit our metric (byte mode / count mode).
+ */
+ switch (mode) {
+ case MODE_BYTES:
+ reps -= ret;
+ if (reps == 0)
+ return (0);
+ break;
+ case MODE_COUNT:
+ reps--;
+ if (reps == 0)
+ return (0);
+ break;
+ default:
+ break;
+ }
+ }
+
+ return (0);
+}
diff --git a/tests/sys/kern/tty/test_canon.orch b/tests/sys/kern/tty/test_canon.orch
new file mode 100644
index 000000000000..28018edfdcd6
--- /dev/null
+++ b/tests/sys/kern/tty/test_canon.orch
@@ -0,0 +1,102 @@
+#!/usr/bin/env -S porch -f
+--
+-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+--
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+
+timeout(3)
+
+spawn("cat")
+
+write "Complete\r"
+match "Complete\r"
+
+write "Basic\rIncomplete"
+match "Basic\r"
+
+-- We shouldn't see any of the "Incomplete" line
+fail(function()
+end)
+
+match "Incomp" {
+ callback = function()
+ exit(1)
+ end
+}
+
+fail(nil)
+
+-- Pushing a ^D along should force a flush of the tty, cat(1) will write the
+-- result without a trailing newline.
+write " line^D"
+match "Incomplete line$"
+
+-- Erase!
+write "Dog^H^D"
+match "Do$"
+
+-- More erase!
+write "Cat Dog^W^D"
+match "Cat $"
+
+write "^D"
+eof()
+
+local function fionread_test(str, expected)
+ spawn("fionread")
+
+ write(str)
+ match(expected)
+end
+
+-- Incomplete line
+fionread_test("Hello", "0")
+-- VEOF does not count
+fionread_test("Hello^D", "5")
+-- VEOF still doesn't count, even if the next line is an extra VEOF later
+fionread_test("Hello^D^D", "5")
+-- read(2) definitely won't return the second incomplete line
+fionread_test("Hello^Dther", "5")
+-- read(2) also won't return a second complete line at once
+fionread_test("Hello^Dthere^D", "5")
+-- Finally, send a VEOF to terminate a blank line and signal EOF in read(2)
+fionread_test("^D", "0")
+
+-- \r will instead show up in the input stream to the application, so we must
+-- make sure those are counted where VEOF generally wouldn't be.
+fionread_test("Hello\r", "6")
+fionread_test("Hello\rther", "6")
+fionread_test("Hello\rthere\r", "6")
+fionread_test("\r", "1")
+
+local function readsz_test(str, arg, expected)
+ spawn("readsz", table.unpack(arg))
+
+ if type(str) == "table" then
+ assert(#str == 2)
+ write(str[1])
+ release()
+
+ -- Give readsz a chance to consume the partial input before we send more
+ -- along.
+ sleep(1)
+ write(str[2])
+ else
+ write(str)
+ end
+ match(expected)
+end
+
+readsz_test("partial", {"-b", 3}, "^$")
+readsz_test("partial^D", {"-b", 3}, "^par$")
+readsz_test("partial^D", {"-c", 1}, "^partial$")
+for s = 1, #"partial" do
+ readsz_test("partial^D", {"-s", s}, "^partial$")
+end
+-- Send part of the line, release and pause, then finish it.
+readsz_test({"par", "tial^D"}, {"-c", 1}, "^partial$")
+-- line is incomplete, so we'll just see the "partial" even if we want two
+readsz_test("partial^Dline", {"-c", 2}, "^partial$")
+readsz_test("partial^Dline^D", {"-c", 1}, "^partial$")
+readsz_test("partial^Dline^D", {"-c", 2}, "^partialline$")
diff --git a/tests/sys/kern/tty/test_canon_fullbuf.orch b/tests/sys/kern/tty/test_canon_fullbuf.orch
new file mode 100644
index 000000000000..1833703e4f45
--- /dev/null
+++ b/tests/sys/kern/tty/test_canon_fullbuf.orch
@@ -0,0 +1,23 @@
+#!/usr/bin/env -S porch -f
+--
+-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+--
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+
+timeout(3)
+
+local TTYINQ_DATASIZE = 128
+local scream = string.rep("A", TTYINQ_DATASIZE - 1)
+
+spawn("cat")
+
+-- Fill up a whole block with screaming + VEOF
+write(scream .. "^D")
+match(scream .. "$")
+
+scream = scream .. "A"
+
+-- Now fill up the next block, but spill the VEOF over to a third block.
+write(scream .. "^D")
+match(scream .. "$")
diff --git a/tests/sys/kern/tty/test_ncanon.orch b/tests/sys/kern/tty/test_ncanon.orch
new file mode 100644
index 000000000000..14a34d82fa9a
--- /dev/null
+++ b/tests/sys/kern/tty/test_ncanon.orch
@@ -0,0 +1,39 @@
+#!/usr/bin/env -S porch -f
+--
+-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+--
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+
+timeout(3)
+
+local function spawn_one(...)
+ spawn(...)
+
+ stty("lflag", 0, tty.lflag.ICANON)
+end
+
+-- We can send one byte...
+spawn_one("readsz", "-c", 1)
+write "H"
+match "^H$"
+
+-- or many.
+spawn_one("readsz", "-c", 1)
+write "Hello"
+match "^Hello$"
+
+-- VEOF is a normal character here, passed through as-is.
+spawn_one("readsz", "-c", 1)
+write "Hello^D"
+match "^Hello\x04$"
+spawn_one("readsz", "-c", 1)
+write "^D"
+match "^\x04$"
+
+-- Confirm that FIONREAD agrees that VEOF will be returned, even if it was sent
+-- while the tty was still in canonical mode.
+spawn("fionread")
+write "^D"
+stty("lflag", 0, tty.lflag.ICANON)
+match "^1$"
diff --git a/tests/sys/kern/tty/test_recanon.orch b/tests/sys/kern/tty/test_recanon.orch
new file mode 100644
index 000000000000..e3943495ca5d
--- /dev/null
+++ b/tests/sys/kern/tty/test_recanon.orch
@@ -0,0 +1,90 @@
+#!/usr/bin/env -S porch -f
+--
+-- Copyright (c) 2024 Kyle Evans <kevans@FreeBSD.org>
+--
+-- SPDX-License-Identifier: BSD-2-Clause
+--
+
+timeout(3)
+
+local TTYINQ_DATASIZE = 128
+local scream = string.rep("A", TTYINQ_DATASIZE - 1)
+
+local function ncanon()
+ stty("lflag", nil, tty.lflag.ICANON)
+end
+
+local function canon()
+ stty("lflag", tty.lflag.ICANON)
+end
+
+spawn("readsz", "-e")
+ncanon()
+
+-- Fill up a whole block with screaming + VEOF; when it gets recanonicalized,
+-- the next line should be pointing to the beginning of the next block.
+write(scream .. "^D")
+
+canon()
+match(scream .. "$")
+
+-- The same as above, but spilling VEOF over to the next block.
+spawn("readsz", "-e")
+ncanon()
+
+write(scream .. "A^D")
+
+canon()
+match(scream .. "A$")
+
+-- We'll do it again, except with one character spilled over to the next block
+-- before we recanonicalize. We should then have the scream, followed by a
+-- partial line containing the spill over.
+spawn("cat")
+ncanon()
+
+write(scream .. "^DZ")
+
+canon()
+match(scream .. "$")
+
+-- Sending "B^D" should give us "ZB" to make sure that we didn't lose anything
+-- at the beginning of the next block.
+
+write("B^D")
+match("^ZB$")
+
+-- Next we'll VEOF at the beginning.
+spawn("readsz", "-e")
+ncanon()
+
+write("^D")
+match("^$")
+
+-- Finally, we'll trigger recanonicalization with an empty buffer. This one is
+-- just about avoiding a panic.
+spawn("true")
+
+ncanon()
+canon()
+release()
+eof()
+
+spawn("readsz", "-c", "1")
+
+write("Test^Dfoo")
+ncanon()
+
+match("^Test\x04foo$")
+
+-- Finally, swap VEOF out with ^F; before recent changes, we would remain
+-- canonicalized at Test^D and the kernel would block on it unless a short
+-- buffer was used since VEOF would not appear within the canonicalized bit.
+spawn("readsz", "-c", 1)
+
+write("Test^DLine^F")
+stty("cc", {
+ VEOF = "^F"
+})
+
+match("^Test\x04Line$")
diff --git a/tests/sys/kern/tty/test_sti.c b/tests/sys/kern/tty/test_sti.c
new file mode 100644
index 000000000000..f792001b4e3f
--- /dev/null
+++ b/tests/sys/kern/tty/test_sti.c
@@ -0,0 +1,337 @@
+/*-
+ * Copyright (c) 2025 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/param.h>
+#include <sys/ioctl.h>
+#include <sys/wait.h>
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <termios.h>
+
+#include <atf-c.h>
+#include <libutil.h>
+
+enum stierr {
+ STIERR_CONFIG_FETCH,
+ STIERR_CONFIG,
+ STIERR_INJECT,
+ STIERR_READFAIL,
+ STIERR_BADTEXT,
+ STIERR_DATAFOUND,
+ STIERR_ROTTY,
+ STIERR_WOTTY,
+ STIERR_WOOK,
+ STIERR_BADERR,
+
+ STIERR_MAXERR
+};
+
+static const struct stierr_map {
+ enum stierr stierr;
+ const char *msg;
+} stierr_map[] = {
+ { STIERR_CONFIG_FETCH, "Failed to fetch ctty configuration" },
+ { STIERR_CONFIG, "Failed to configure ctty in the child" },
+ { STIERR_INJECT, "Failed to inject characters via TIOCSTI" },
+ { STIERR_READFAIL, "Failed to read(2) from stdin" },
+ { STIERR_BADTEXT, "read(2) data did not match injected data" },
+ { STIERR_DATAFOUND, "read(2) data when we did not expected to" },
+ { STIERR_ROTTY, "Failed to open tty r/o" },
+ { STIERR_WOTTY, "Failed to open tty w/o" },
+ { STIERR_WOOK, "TIOCSTI on w/o tty succeeded" },
+ { STIERR_BADERR, "Received wrong error from failed TIOCSTI" },
+};
+_Static_assert(nitems(stierr_map) == STIERR_MAXERR,
+ "Failed to describe all errors");
+
+/*
+ * Inject each character of the input string into the TTY. The caller can
+ * assume that errno is preserved on return.
+ */
+static ssize_t
+inject(int fileno, const char *str)
+{
+ size_t nb = 0;
+
+ for (const char *walker = str; *walker != '\0'; walker++) {
+ if (ioctl(fileno, TIOCSTI, walker) != 0)
+ return (-1);
+ nb++;
+ }
+
+ return (nb);
+}
+
+/*
+ * Forks off a new process, stashes the parent's handle for the pty in *termfd
+ * and returns the pid. 0 for the child, >0 for the parent, as usual.
+ *
+ * Most tests fork so that we can do them while unprivileged, which we can only
+ * do if we're operating on our ctty (and we don't want to touch the tty of
+ * whatever may be running the tests).
+ */
+static int
+init_pty(int *termfd, bool canon)
+{
+ int pid;
+
+ pid = forkpty(termfd, NULL, NULL, NULL);
+ ATF_REQUIRE(pid != -1);
+
+ if (pid == 0) {
+ struct termios term;
+
+ /*
+ * Child reconfigures tty to disable echo and put it into raw
+ * mode if requested.
+ */
+ if (tcgetattr(STDIN_FILENO, &term) == -1)
+ _exit(STIERR_CONFIG_FETCH);
+ term.c_lflag &= ~ECHO;
+ if (!canon)
+ term.c_lflag &= ~ICANON;
+ if (tcsetattr(STDIN_FILENO, TCSANOW, &term) == -1)
+ _exit(STIERR_CONFIG);
+ }
+
+ return (pid);
+}
+
+static void
+finalize_child(pid_t pid, int signo)
+{
+ int status, wpid;
+
+ while ((wpid = waitpid(pid, &status, 0)) != pid) {
+ if (wpid != -1)
+ continue;
+ ATF_REQUIRE_EQ_MSG(EINTR, errno,
+ "waitpid: %s", strerror(errno));
+ }
+
+ /*
+ * Some tests will signal the child for whatever reason, and we're
+ * expecting it to terminate it. For those cases, it's OK to just see
+ * that termination. For all other cases, we expect a graceful exit
+ * with an exit status that reflects a cause that we have an error
+ * mapped for.
+ */
+ if (signo >= 0) {
+ ATF_REQUIRE(WIFSIGNALED(status));
+ ATF_REQUIRE_EQ(signo, WTERMSIG(status));
+ } else {
+ ATF_REQUIRE(WIFEXITED(status));
+ if (WEXITSTATUS(status) != 0) {
+ int err = WEXITSTATUS(status);
+
+ for (size_t i = 0; i < nitems(stierr_map); i++) {
+ const struct stierr_map *map = &stierr_map[i];
+
+ if ((int)map->stierr == err) {
+ atf_tc_fail("%s", map->msg);
+ __assert_unreachable();
+ }
+ }
+ }
+ }
+}
+
+ATF_TC(basic);
+ATF_TC_HEAD(basic, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test for basic functionality of TIOCSTI");
+ atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(basic, tc)
+{
+ int pid, term;
+
+ /*
+ * We don't canonicalize on this test because we can assume that the
+ * injected data will be available after TIOCSTI returns. This is all
+ * within a single thread for the basic test, so we simplify our lives
+ * slightly in raw mode.
+ */
+ pid = init_pty(&term, false);
+ if (pid == 0) {
+ static const char sending[] = "Text";
+ char readbuf[32];
+ ssize_t injected, readsz;
+
+ injected = inject(STDIN_FILENO, sending);
+ if (injected != sizeof(sending) - 1)
+ _exit(STIERR_INJECT);
+
+ readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
+
+ if (readsz < 0 || readsz != injected)
+ _exit(STIERR_READFAIL);
+ if (memcmp(readbuf, sending, readsz) != 0)
+ _exit(STIERR_BADTEXT);
+
+ _exit(0);
+ }
+
+ finalize_child(pid, -1);
+}
+
+ATF_TC(root);
+ATF_TC_HEAD(root, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test that root can inject into another TTY");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(root, tc)
+{
+ static const char sending[] = "Text\r";
+ ssize_t injected;
+ int pid, term;
+
+ /*
+ * We leave canonicalization enabled for this one so that the read(2)
+ * below hangs until we have all of the data available, rather than
+ * having to signal OOB that it's safe to read.
+ */
+ pid = init_pty(&term, true);
+ if (pid == 0) {
+ char readbuf[32];
+ ssize_t readsz;
+
+ readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
+ if (readsz < 0 || readsz != sizeof(sending) - 1)
+ _exit(STIERR_READFAIL);
+
+ /*
+ * Here we ignore the trailing \r, because it won't have
+ * surfaced in our read(2).
+ */
+ if (memcmp(readbuf, sending, readsz - 1) != 0)
+ _exit(STIERR_BADTEXT);
+
+ _exit(0);
+ }
+
+ injected = inject(term, sending);
+ ATF_REQUIRE_EQ_MSG(sizeof(sending) - 1, injected,
+ "Injected %zu characters, expected %zu", injected,
+ sizeof(sending) - 1);
+
+ finalize_child(pid, -1);
+}
+
+ATF_TC(unprivileged_fail_noctty);
+ATF_TC_HEAD(unprivileged_fail_noctty, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test that unprivileged cannot inject into non-controlling TTY");
+ atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(unprivileged_fail_noctty, tc)
+{
+ const char sending[] = "Text";
+ ssize_t injected;
+ int pid, serrno, term;
+
+ pid = init_pty(&term, false);
+ if (pid == 0) {
+ char readbuf[32];
+ ssize_t readsz;
+
+ /*
+ * This should hang until we get terminated by the parent.
+ */
+ readsz = read(STDIN_FILENO, readbuf, sizeof(readbuf));
+ if (readsz > 0)
+ _exit(STIERR_DATAFOUND);
+
+ _exit(0);
+ }
+
+ /* Should fail. */
+ injected = inject(term, sending);
+ serrno = errno;
+
+ /* Done with the child, just kill it now to avoid problems later. */
+ kill(pid, SIGINT);
+ finalize_child(pid, SIGINT);
+
+ ATF_REQUIRE_EQ_MSG(-1, (ssize_t)injected,
+ "TIOCSTI into non-ctty succeeded");
+ ATF_REQUIRE_EQ(EACCES, serrno);
+}
+
+ATF_TC(unprivileged_fail_noread);
+ATF_TC_HEAD(unprivileged_fail_noread, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Test that unprivileged cannot inject into TTY not opened for read");
+ atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(unprivileged_fail_noread, tc)
+{
+ int pid, term;
+
+ /*
+ * Canonicalization actually doesn't matter for this one, we'll trust
+ * that the failure means we didn't inject anything.
+ */
+ pid = init_pty(&term, true);
+ if (pid == 0) {
+ static const char sending[] = "Text";
+ ssize_t injected;
+ int rotty, wotty;
+
+ /*
+ * We open the tty both r/o and w/o to ensure we got the device
+ * name right; one of these will pass, one of these will fail.
+ */
+ wotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_WRONLY);
+ if (wotty == -1)
+ _exit(STIERR_WOTTY);
+ rotty = openat(STDIN_FILENO, "", O_EMPTY_PATH | O_RDONLY);
+ if (rotty == -1)
+ _exit(STIERR_ROTTY);
+
+ /*
+ * This injection is expected to fail with EPERM, because it may
+ * be our controlling tty but it is not open for reading.
+ */
+ injected = inject(wotty, sending);
+ if (injected != -1)
+ _exit(STIERR_WOOK);
+ if (errno != EPERM)
+ _exit(STIERR_BADERR);
+
+ /*
+ * Demonstrate that it does succeed on the other fd we opened,
+ * which is r/o.
+ */
+ injected = inject(rotty, sending);
+ if (injected != sizeof(sending) - 1)
+ _exit(STIERR_INJECT);
+
+ _exit(0);
+ }
+
+ finalize_child(pid, -1);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, basic);
+ ATF_TP_ADD_TC(tp, root);
+ ATF_TP_ADD_TC(tp, unprivileged_fail_noctty);
+ ATF_TP_ADD_TC(tp, unprivileged_fail_noread);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/kern/unix_dgram.c b/tests/sys/kern/unix_dgram.c
index 9df0d4ca7168..7464cdf197cd 100644
--- a/tests/sys/kern/unix_dgram.c
+++ b/tests/sys/kern/unix_dgram.c
@@ -25,13 +25,15 @@
* SUCH DAMAGE.
*/
-#include <sys/time.h>
#include <sys/event.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <sys/socket.h>
+#include <sys/stat.h>
#include <sys/sysctl.h>
+#include <sys/time.h>
#include <sys/un.h>
+
#include <aio.h>
#include <errno.h>
#include <fcntl.h>
@@ -391,12 +393,49 @@ ATF_TC_BODY(selfgetpeername, tc)
ATF_REQUIRE(close(sd) == 0);
}
+ATF_TC_WITHOUT_HEAD(fchmod);
+ATF_TC_BODY(fchmod, tc)
+{
+ struct stat sb;
+ struct sockaddr_un sun;
+ int error, sd;
+
+ memset(&sun, 0, sizeof(sun));
+ sun.sun_len = sizeof(sun);
+ sun.sun_family = AF_UNIX;
+ strlcpy(sun.sun_path, "sock", sizeof(sun.sun_path));
+
+ sd = socket(PF_UNIX, SOCK_DGRAM, 0);
+ ATF_REQUIRE(sd != -1);
+
+ error = fchmod(sd, 0600 | S_ISUID);
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+
+ umask(0022);
+ error = fchmod(sd, 0766);
+ ATF_REQUIRE(error == 0);
+
+ error = bind(sd, (struct sockaddr *)&sun, sizeof(sun));
+ ATF_REQUIRE(error == 0);
+
+ error = stat(sun.sun_path, &sb);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE_MSG((sb.st_mode & 0777) == 0744,
+ "sb.st_mode = %o", sb.st_mode);
+
+ error = fchmod(sd, 0666);
+ ATF_REQUIRE_ERRNO(EINVAL, error == -1);
+
+ ATF_REQUIRE(close(sd) == 0);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, basic);
ATF_TP_ADD_TC(tp, one2many);
ATF_TP_ADD_TC(tp, event);
ATF_TP_ADD_TC(tp, selfgetpeername);
+ ATF_TP_ADD_TC(tp, fchmod);
return (atf_no_error());
}
diff --git a/tests/sys/kern/unix_passfd_test.c b/tests/sys/kern/unix_passfd_test.c
index 74095859d899..95271c04a16b 100644
--- a/tests/sys/kern/unix_passfd_test.c
+++ b/tests/sys/kern/unix_passfd_test.c
@@ -27,15 +27,19 @@
*/
#include <sys/param.h>
+#include <sys/jail.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/un.h>
+#include <sys/wait.h>
+#include <err.h>
#include <errno.h>
#include <fcntl.h>
+#include <jail.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
@@ -544,6 +548,51 @@ ATF_TC_BODY(send_overflow, tc)
closesocketpair(fd);
}
+/*
+ * Make sure that we do not receive descriptors with MSG_PEEK.
+ */
+ATF_TC_WITHOUT_HEAD(peek);
+ATF_TC_BODY(peek, tc)
+{
+ int fd[2], getfd, putfd, nfds;
+
+ domainsocketpair(fd);
+ tempfile(&putfd);
+ nfds = getnfds();
+ sendfd(fd[0], putfd);
+ ATF_REQUIRE(getnfds() == nfds);
+
+ /* First make MSG_PEEK recvmsg(2)... */
+ char cbuf[CMSG_SPACE(sizeof(int))];
+ char buf[1];
+ struct iovec iov = {
+ .iov_base = buf,
+ .iov_len = sizeof(buf)
+ };
+ struct msghdr msghdr = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ ATF_REQUIRE(1 == recvmsg(fd[1], &msghdr, MSG_PEEK));
+ for (struct cmsghdr *cmsghdr = CMSG_FIRSTHDR(&msghdr);
+ cmsghdr != NULL; cmsghdr = CMSG_NXTHDR(&msghdr, cmsghdr)) {
+ /* Usually this is some garbage. */
+ printf("level %d type %d len %u\n",
+ cmsghdr->cmsg_level, cmsghdr->cmsg_type, cmsghdr->cmsg_len);
+ }
+
+ /* ... and make sure we did not receive any descriptors! */
+ ATF_REQUIRE(getnfds() == nfds);
+
+ /* Now really receive a descriptor. */
+ recvfd(fd[1], &getfd, 0);
+ ATF_REQUIRE(getnfds() == nfds + 1);
+ close(putfd);
+ close(getfd);
+ closesocketpair(fd);
+}
/*
* Send two files. Then receive them. Make sure they are returned in the
@@ -987,6 +1036,135 @@ ATF_TC_BODY(control_creates_records, tc)
closesocketpair(fd);
}
+ATF_TC_WITH_CLEANUP(cross_jail_dirfd);
+ATF_TC_HEAD(cross_jail_dirfd, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(cross_jail_dirfd, tc)
+{
+ int error, sock[2], jid1, jid2, status;
+ pid_t pid1, pid2;
+
+ domainsocketpair(sock);
+
+ error = mkdir("./a", 0755);
+ ATF_REQUIRE(error == 0);
+ error = mkdir("./b", 0755);
+ ATF_REQUIRE(error == 0);
+ error = mkdir("./c", 0755);
+ ATF_REQUIRE(error == 0);
+ error = mkdir("./a/c", 0755);
+ ATF_REQUIRE(error == 0);
+
+ jid1 = jail_setv(JAIL_CREATE,
+ "name", "passfd_test_cross_jail_dirfd1",
+ "path", "./a",
+ "persist", NULL,
+ NULL);
+ ATF_REQUIRE_MSG(jid1 >= 0, "jail_setv: %s", jail_errmsg);
+
+ jid2 = jail_setv(JAIL_CREATE,
+ "name", "passfd_test_cross_jail_dirfd2",
+ "path", "./b",
+ "persist", NULL,
+ NULL);
+ ATF_REQUIRE_MSG(jid2 >= 0, "jail_setv: %s", jail_errmsg);
+
+ pid1 = fork();
+ ATF_REQUIRE(pid1 >= 0);
+ if (pid1 == 0) {
+ ssize_t len;
+ int dfd, error;
+ char ch;
+
+ error = jail_attach(jid1);
+ if (error != 0)
+ err(1, "jail_attach");
+
+ dfd = open(".", O_RDONLY | O_DIRECTORY);
+ if (dfd < 0)
+ err(1, "open(\".\") in jail %d", jid1);
+
+ ch = 0;
+ len = sendfd_payload(sock[0], dfd, &ch, sizeof(ch));
+ if (len == -1)
+ err(1, "sendmsg");
+
+ _exit(0);
+ }
+
+ pid2 = fork();
+ ATF_REQUIRE(pid2 >= 0);
+ if (pid2 == 0) {
+ ssize_t len;
+ int dfd, dfd2, error, fd;
+ char ch;
+
+ error = jail_attach(jid2);
+ if (error != 0)
+ err(1, "jail_attach");
+
+ /* Get a directory from outside the jail root. */
+ len = recvfd_payload(sock[1], &dfd, &ch, sizeof(ch),
+ CMSG_SPACE(sizeof(int)), 0);
+ if (len == -1)
+ err(1, "recvmsg");
+
+ if ((fcntl(dfd, F_GETFD) & FD_RESOLVE_BENEATH) == 0)
+ errx(1, "dfd does not have FD_RESOLVE_BENEATH set");
+
+ /* Make sure we can't chdir. */
+ error = fchdir(dfd);
+ if (error == 0)
+ errx(1, "fchdir succeeded");
+ if (errno != ENOTCAPABLE)
+ err(1, "fchdir");
+
+ /* Make sure a dotdot access fails. */
+ fd = openat(dfd, "../c", O_RDONLY | O_DIRECTORY);
+ if (fd >= 0)
+ errx(1, "openat(\"../c\") succeeded");
+ if (errno != ENOTCAPABLE)
+ err(1, "openat");
+
+ /* Accesses within the sender's jail root are ok. */
+ fd = openat(dfd, "c", O_RDONLY | O_DIRECTORY);
+ if (fd < 0)
+ err(1, "openat(\"c\")");
+
+ dfd2 = openat(dfd, "", O_EMPTY_PATH | O_RDONLY | O_DIRECTORY);
+ if (dfd2 < 0)
+ err(1, "openat(\"\")");
+ if ((fcntl(dfd2, F_GETFD) & FD_RESOLVE_BENEATH) == 0)
+ errx(1, "dfd2 does not have FD_RESOLVE_BENEATH set");
+
+ _exit(0);
+ }
+
+ error = waitpid(pid1, &status, 0);
+ ATF_REQUIRE(error != -1);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 0);
+ error = waitpid(pid2, &status, 0);
+ ATF_REQUIRE(error != -1);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 0);
+
+ closesocketpair(sock);
+}
+ATF_TC_CLEANUP(cross_jail_dirfd, tc)
+{
+ int jid;
+
+ jid = jail_getid("passfd_test_cross_jail_dirfd1");
+ if (jid >= 0 && jail_remove(jid) != 0)
+ err(1, "jail_remove");
+ jid = jail_getid("passfd_test_cross_jail_dirfd2");
+ if (jid >= 0 && jail_remove(jid) != 0)
+ err(1, "jail_remove");
+}
+
ATF_TP_ADD_TCS(tp)
{
@@ -997,6 +1175,7 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, send_and_shutdown);
ATF_TP_ADD_TC(tp, send_a_lot);
ATF_TP_ADD_TC(tp, send_overflow);
+ ATF_TP_ADD_TC(tp, peek);
ATF_TP_ADD_TC(tp, two_files);
ATF_TP_ADD_TC(tp, bundle);
ATF_TP_ADD_TC(tp, bundle_cancel);
@@ -1006,6 +1185,7 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, copyout_rights_error);
ATF_TP_ADD_TC(tp, empty_rights_message);
ATF_TP_ADD_TC(tp, control_creates_records);
+ ATF_TP_ADD_TC(tp, cross_jail_dirfd);
return (atf_no_error());
}
diff --git a/tests/sys/kern/unix_seqpacket_test.c b/tests/sys/kern/unix_seqpacket_test.c
index dc5f89270a07..b9a6be015241 100644
--- a/tests/sys/kern/unix_seqpacket_test.c
+++ b/tests/sys/kern/unix_seqpacket_test.c
@@ -894,6 +894,38 @@ ATF_TC_BODY(shutdown_send_sigpipe, tc)
close(s2);
}
+/*
+ * https://syzkaller.appspot.com/bug?id=ac94349a29f2efc40e9274239e4ca9b2c473a4e7
+ */
+ATF_TC_WITHOUT_HEAD(shutdown_o_async);
+ATF_TC_BODY(shutdown_o_async, tc)
+{
+ int sv[2];
+
+ do_socketpair(sv);
+
+ ATF_CHECK_EQ(0, fcntl(sv[0], F_SETFL, O_ASYNC));
+ ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR));
+ close(sv[0]);
+ close(sv[1]);
+}
+
+/*
+ * If peer had done SHUT_WR on their side, our recv(2) shouldn't block.
+ */
+ATF_TC_WITHOUT_HEAD(shutdown_recv);
+ATF_TC_BODY(shutdown_recv, tc)
+{
+ char buf[10];
+ int sv[2];
+
+ do_socketpair(sv);
+ ATF_CHECK_EQ(0, shutdown(sv[0], SHUT_WR));
+ ATF_CHECK_EQ(0, recv(sv[1], buf, sizeof(buf), 0));
+ close(sv[0]);
+ close(sv[1]);
+}
+
/* nonblocking send(2) and recv(2) a single short record */
ATF_TC_WITHOUT_HEAD(send_recv_nonblocking);
ATF_TC_BODY(send_recv_nonblocking, tc)
@@ -1207,7 +1239,7 @@ ATF_TC_BODY(random_eor_and_waitall, tc)
for (u_int i = 0; i < RANDOM_TESTSIZE / (u_int )sizeof(long); i++)
((long *)params.sendbuf)[i] = nrand48(&params.seed[0]);
- ATF_REQUIRE(sysctlbyname("net.local.stream.recvspace",
+ ATF_REQUIRE(sysctlbyname("net.local.seqpacket.recvspace",
&params.recvspace, &(size_t){sizeof(u_long)}, NULL, 0) != -1);
ATF_REQUIRE((recvbuf =
malloc(RANDOM_RECVSIZE * params.recvspace)) != NULL);
@@ -1310,6 +1342,8 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, implied_connect);
ATF_TP_ADD_TC(tp, shutdown_send);
ATF_TP_ADD_TC(tp, shutdown_send_sigpipe);
+ ATF_TP_ADD_TC(tp, shutdown_o_async);
+ ATF_TP_ADD_TC(tp, shutdown_recv);
ATF_TP_ADD_TC(tp, eagain_8k_8k);
ATF_TP_ADD_TC(tp, eagain_8k_128k);
ATF_TP_ADD_TC(tp, eagain_128k_8k);
diff --git a/tests/sys/kern/unix_stream.c b/tests/sys/kern/unix_stream.c
index d57cfad020fa..bb811f78f620 100644
--- a/tests/sys/kern/unix_stream.c
+++ b/tests/sys/kern/unix_stream.c
@@ -26,14 +26,18 @@
*/
#include <sys/cdefs.h>
-#include <errno.h>
-#include <fcntl.h>
-#include <pthread.h>
-#include <signal.h>
#include <sys/socket.h>
+#include <sys/event.h>
+#include <sys/select.h>
+#include <sys/sysctl.h>
#include <sys/un.h>
-
+#include <errno.h>
+#include <fcntl.h>
#include <stdio.h>
+#include <stdlib.h>
+#include <poll.h>
+#include <pthread.h>
+#include <pthread_np.h>
#include <atf-c.h>
@@ -49,6 +53,18 @@ do_socketpair(int *sv)
ATF_REQUIRE(sv[0] != sv[1]);
}
+static u_long
+getsendspace(void)
+{
+ u_long sendspace;
+
+ ATF_REQUIRE_MSG(sysctlbyname("net.local.stream.sendspace", &sendspace,
+ &(size_t){sizeof(u_long)}, NULL, 0) != -1,
+ "sysctl net.local.stream.sendspace failed: %s", strerror(errno));
+
+ return (sendspace);
+}
+
/* getpeereid(3) should work with stream sockets created via socketpair(2) */
ATF_TC_WITHOUT_HEAD(getpeereid);
ATF_TC_BODY(getpeereid, tc)
@@ -86,11 +102,386 @@ ATF_TC_BODY(send_0, tc)
close(sv[1]);
}
+struct check_ctx;
+typedef void check_func_t(struct check_ctx *);
+struct check_ctx {
+ check_func_t *method;
+ int sv[2];
+ bool timeout;
+ union {
+ enum { SELECT_RD, SELECT_WR } select_what;
+ short poll_events;
+ short kev_filter;
+ };
+ int nfds;
+ union {
+ short poll_revents;
+ unsigned short kev_flags;
+ };
+};
+
+static void
+check_select(struct check_ctx *ctx)
+{
+ fd_set fds;
+ int nfds;
+
+ FD_ZERO(&fds);
+ FD_SET(ctx->sv[0], &fds);
+ nfds = select(ctx->sv[0] + 1,
+ ctx->select_what == SELECT_RD ? &fds : NULL,
+ ctx->select_what == SELECT_WR ? &fds : NULL,
+ NULL,
+ ctx->timeout ? &(struct timeval){.tv_usec = 1000} : NULL);
+ ATF_REQUIRE_MSG(nfds == ctx->nfds,
+ "select() returns %d errno %d", nfds, errno);
+}
+
+static void
+check_poll(struct check_ctx *ctx)
+{
+ struct pollfd pfd[1];
+ int nfds;
+
+ pfd[0] = (struct pollfd){
+ .fd = ctx->sv[0],
+ .events = ctx->poll_events,
+ };
+ nfds = poll(pfd, 1, ctx->timeout ? 1 : INFTIM);
+ ATF_REQUIRE_MSG(nfds == ctx->nfds,
+ "poll() returns %d errno %d", nfds, errno);
+ ATF_REQUIRE((pfd[0].revents & ctx->poll_revents) == ctx->poll_revents);
+}
+
+static void
+check_kevent(struct check_ctx *ctx)
+{
+ struct kevent kev;
+ int nfds, kq;
+
+ ATF_REQUIRE(kq = kqueue());
+ EV_SET(&kev, ctx->sv[0], ctx->kev_filter, EV_ADD, 0, 0, NULL);
+ nfds = kevent(kq, &kev, 1, NULL, 0, NULL);
+ ATF_REQUIRE_MSG(nfds == 0,
+ "kevent() returns %d errno %d", nfds, errno);
+ nfds = kevent(kq, NULL, 0, &kev, 1, ctx->timeout ?
+ &(struct timespec){.tv_nsec = 1000000} : NULL);
+ ATF_REQUIRE_MSG(nfds == ctx->nfds,
+ "kevent() returns %d errno %d", nfds, errno);
+ ATF_REQUIRE(kev.ident == (uintptr_t)ctx->sv[0] &&
+ kev.filter == ctx->kev_filter &&
+ (kev.flags & ctx->kev_flags) == ctx->kev_flags);
+ close(kq);
+}
+
+static void
+full_socketpair(int *sv)
+{
+ void *buf;
+ u_long sendspace;
+
+ sendspace = getsendspace();
+ ATF_REQUIRE((buf = malloc(sendspace)) != NULL);
+ do_socketpair(sv);
+ ATF_REQUIRE(fcntl(sv[0], F_SETFL, O_NONBLOCK) != -1);
+ do {} while (send(sv[0], buf, sendspace, 0) == (ssize_t)sendspace);
+ ATF_REQUIRE(errno == EAGAIN);
+ ATF_REQUIRE(fcntl(sv[0], F_SETFL, 0) != -1);
+ free(buf);
+}
+
+static void *
+pthread_wrap(void *arg)
+{
+ struct check_ctx *ctx = arg;
+
+ ctx->method(ctx);
+
+ return (NULL);
+}
+
+/*
+ * Launch a thread that would block in event mech and return it.
+ */
+static pthread_t
+pthread_create_blocked(struct check_ctx *ctx)
+{
+ pthread_t thr;
+
+ ctx->timeout = false;
+ ctx->nfds = 1;
+ ATF_REQUIRE(pthread_create(&thr, NULL, pthread_wrap, ctx) == 0);
+
+ /* Sleep a bit to make sure that thread is put to sleep. */
+ usleep(10000);
+ ATF_REQUIRE(pthread_peekjoin_np(thr, NULL) == EBUSY);
+
+ return (thr);
+}
+
+static void
+full_writability_check(struct check_ctx *ctx)
+{
+ pthread_t thr;
+ void *buf;
+ u_long space;
+
+ space = getsendspace() / 2;
+ ATF_REQUIRE((buf = malloc(space)) != NULL);
+
+ /* First check with timeout, expecting 0 fds returned. */
+ ctx->timeout = true;
+ ctx->nfds = 0;
+ ctx->method(ctx);
+
+ thr = pthread_create_blocked(ctx);
+
+ /* Read some data and re-check, the fd is expected to be returned. */
+ ATF_REQUIRE(read(ctx->sv[1], buf, space) == (ssize_t)space);
+
+ /* Now check that thread was successfully woken up and exited. */
+ ATF_REQUIRE(pthread_join(thr, NULL) == 0);
+
+ /* Extra check repeating what joined thread already did. */
+ ctx->method(ctx);
+
+ close(ctx->sv[0]);
+ close(ctx->sv[1]);
+ free(buf);
+}
+
+/*
+ * Make sure that a full socket is not reported as writable by event APIs.
+ */
+ATF_TC_WITHOUT_HEAD(full_writability_select);
+ATF_TC_BODY(full_writability_select, tc)
+{
+ struct check_ctx ctx = {
+ .method = check_select,
+ .select_what = SELECT_WR,
+ };
+
+ full_socketpair(ctx.sv);
+ full_writability_check(&ctx);
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(full_writability_poll);
+ATF_TC_BODY(full_writability_poll, tc)
+{
+ struct check_ctx ctx = {
+ .method = check_poll,
+ .poll_events = POLLOUT | POLLWRNORM,
+ };
+
+ full_socketpair(ctx.sv);
+ full_writability_check(&ctx);
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(full_writability_kevent);
+ATF_TC_BODY(full_writability_kevent, tc)
+{
+ struct check_ctx ctx = {
+ .method = check_kevent,
+ .kev_filter = EVFILT_WRITE,
+ };
+
+ full_socketpair(ctx.sv);
+ full_writability_check(&ctx);
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(connected_writability);
+ATF_TC_BODY(connected_writability, tc)
+{
+ struct check_ctx ctx = {
+ .timeout = true,
+ .nfds = 1,
+ };
+
+ do_socketpair(ctx.sv);
+
+ ctx.select_what = SELECT_WR;
+ check_select(&ctx);
+ ctx.poll_events = POLLOUT | POLLWRNORM;
+ check_poll(&ctx);
+ ctx.kev_filter = EVFILT_WRITE;
+ check_kevent(&ctx);
+
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(unconnected_writability);
+ATF_TC_BODY(unconnected_writability, tc)
+{
+ struct check_ctx ctx = {
+ .timeout = true,
+ .nfds = 0,
+ };
+
+ ATF_REQUIRE((ctx.sv[0] = socket(PF_LOCAL, SOCK_STREAM, 0)) > 0);
+
+ ctx.select_what = SELECT_WR;
+ check_select(&ctx);
+ ctx.poll_events = POLLOUT | POLLWRNORM;
+ check_poll(&ctx);
+ ctx.kev_filter = EVFILT_WRITE;
+ check_kevent(&ctx);
+
+ close(ctx.sv[0]);
+}
+
+ATF_TC_WITHOUT_HEAD(peerclosed_writability);
+ATF_TC_BODY(peerclosed_writability, tc)
+{
+ struct check_ctx ctx = {
+ .timeout = false,
+ .nfds = 1,
+ };
+
+ do_socketpair(ctx.sv);
+ close(ctx.sv[1]);
+
+ ctx.select_what = SELECT_WR;
+ check_select(&ctx);
+ ctx.poll_events = POLLOUT | POLLWRNORM;
+ check_poll(&ctx);
+ ctx.kev_filter = EVFILT_WRITE;
+ ctx.kev_flags = EV_EOF;
+ check_kevent(&ctx);
+
+ close(ctx.sv[0]);
+}
+
+ATF_TC_WITHOUT_HEAD(peershutdown_writability);
+ATF_TC_BODY(peershutdown_writability, tc)
+{
+ struct check_ctx ctx = {
+ .timeout = false,
+ .nfds = 1,
+ };
+
+ do_socketpair(ctx.sv);
+ shutdown(ctx.sv[1], SHUT_RD);
+
+ ctx.select_what = SELECT_WR;
+ check_select(&ctx);
+ ctx.poll_events = POLLOUT | POLLWRNORM;
+ check_poll(&ctx);
+ /*
+ * XXXGL: historically unix(4) sockets were not reporting peer's
+ * shutdown(SHUT_RD) as our EV_EOF. The kevent(2) manual page says
+ * "filter will set EV_EOF when the reader disconnects", which is hard
+ * to interpret unambigously. For now leave the historic behavior,
+ * but we may want to change that in uipc_usrreq.c:uipc_filt_sowrite(),
+ * and then this test will also expect EV_EOF in returned flags.
+ */
+ ctx.kev_filter = EVFILT_WRITE;
+ check_kevent(&ctx);
+
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(peershutdown_readability);
+ATF_TC_BODY(peershutdown_readability, tc)
+{
+ struct check_ctx ctx = {
+ .timeout = false,
+ .nfds = 1,
+ };
+ ssize_t readsz;
+ char c;
+
+ do_socketpair(ctx.sv);
+ shutdown(ctx.sv[1], SHUT_WR);
+
+ /*
+ * The other side should flag as readable in select(2) to allow it to
+ * read(2) and observe EOF. Ensure that both poll(2) and select(2)
+ * are consistent here.
+ */
+ ctx.select_what = SELECT_RD;
+ check_select(&ctx);
+ ctx.poll_events = POLLIN | POLLRDNORM;
+ check_poll(&ctx);
+
+ /*
+ * Also check that read doesn't block.
+ */
+ readsz = read(ctx.sv[0], &c, sizeof(c));
+ ATF_REQUIRE_INTEQ(0, readsz);
+
+ close(ctx.sv[0]);
+ close(ctx.sv[1]);
+}
+
+static void
+peershutdown_wakeup(struct check_ctx *ctx)
+{
+ pthread_t thr;
+
+ ctx->timeout = false;
+ ctx->nfds = 1;
+
+ do_socketpair(ctx->sv);
+ thr = pthread_create_blocked(ctx);
+ shutdown(ctx->sv[1], SHUT_WR);
+ ATF_REQUIRE(pthread_join(thr, NULL) == 0);
+
+ close(ctx->sv[0]);
+ close(ctx->sv[1]);
+}
+
+ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_select);
+ATF_TC_BODY(peershutdown_wakeup_select, tc)
+{
+ peershutdown_wakeup(&(struct check_ctx){
+ .method = check_select,
+ .select_what = SELECT_RD,
+ });
+}
+
+ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_poll);
+ATF_TC_BODY(peershutdown_wakeup_poll, tc)
+{
+ peershutdown_wakeup(&(struct check_ctx){
+ .method = check_poll,
+ .poll_events = POLLIN | POLLRDNORM | POLLRDHUP,
+ .poll_revents = POLLRDHUP,
+ });
+}
+
+ATF_TC_WITHOUT_HEAD(peershutdown_wakeup_kevent);
+ATF_TC_BODY(peershutdown_wakeup_kevent, tc)
+{
+ peershutdown_wakeup(&(struct check_ctx){
+ .method = check_kevent,
+ .kev_filter = EVFILT_READ,
+ .kev_flags = EV_EOF,
+ });
+}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, getpeereid);
ATF_TP_ADD_TC(tp, send_0);
+ ATF_TP_ADD_TC(tp, connected_writability);
+ ATF_TP_ADD_TC(tp, unconnected_writability);
+ ATF_TP_ADD_TC(tp, full_writability_select);
+ ATF_TP_ADD_TC(tp, full_writability_poll);
+ ATF_TP_ADD_TC(tp, full_writability_kevent);
+ ATF_TP_ADD_TC(tp, peerclosed_writability);
+ ATF_TP_ADD_TC(tp, peershutdown_writability);
+ ATF_TP_ADD_TC(tp, peershutdown_readability);
+ ATF_TP_ADD_TC(tp, peershutdown_wakeup_select);
+ ATF_TP_ADD_TC(tp, peershutdown_wakeup_poll);
+ ATF_TP_ADD_TC(tp, peershutdown_wakeup_kevent);
return atf_no_error();
}
diff --git a/tests/sys/kqueue/Makefile.inc b/tests/sys/kqueue/Makefile.inc
index d3b5cbd3a79b..01b5f23410c8 100644
--- a/tests/sys/kqueue/Makefile.inc
+++ b/tests/sys/kqueue/Makefile.inc
@@ -1,2 +1 @@
-
.include "../Makefile.inc"
diff --git a/tests/sys/kqueue/libkqueue/Makefile b/tests/sys/kqueue/libkqueue/Makefile
index 797ff98f87a3..31eee2bd7c40 100644
--- a/tests/sys/kqueue/libkqueue/Makefile
+++ b/tests/sys/kqueue/libkqueue/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/kqueue/libkqueue
BINDIR= ${TESTSDIR}
diff --git a/tests/sys/kqueue/libkqueue/timer.c b/tests/sys/kqueue/libkqueue/timer.c
index 523dedc7c800..5116aea98b83 100644
--- a/tests/sys/kqueue/libkqueue/timer.c
+++ b/tests/sys/kqueue/libkqueue/timer.c
@@ -199,7 +199,7 @@ test_periodic_modify(void)
kevent_cmp(&kev, kevent_get(kqfd));
/* Check if the event occurs again */
- EV_SET(&kev, vnode_fd, EVFILT_TIMER, EV_ADD, 0, 500, NULL);
+ EV_SET(&kev, vnode_fd, EVFILT_TIMER, EV_ADD, 0, 495, NULL);
if (kevent(kqfd, &kev, 1, NULL, 0, NULL) < 0)
err(1, "%s", test_id);
diff --git a/tests/sys/mac/Makefile b/tests/sys/mac/Makefile
index 4c38ce1d6c7f..3447d00122f5 100644
--- a/tests/sys/mac/Makefile
+++ b/tests/sys/mac/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/mac
TESTS_SUBDIRS+= bsdextended
diff --git a/tests/sys/mac/Makefile.inc b/tests/sys/mac/Makefile.inc
index d3b5cbd3a79b..01b5f23410c8 100644
--- a/tests/sys/mac/Makefile.inc
+++ b/tests/sys/mac/Makefile.inc
@@ -1,2 +1 @@
-
.include "../Makefile.inc"
diff --git a/tests/sys/mac/bsdextended/Makefile b/tests/sys/mac/bsdextended/Makefile
index f5b07db386c2..69cd27c0e321 100644
--- a/tests/sys/mac/bsdextended/Makefile
+++ b/tests/sys/mac/bsdextended/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/mac/bsdextended
ATF_TESTS_SH+= matches_test
diff --git a/tests/sys/mac/ipacl/Makefile b/tests/sys/mac/ipacl/Makefile
index 4a5dfaa015c3..e083f6c1a69c 100644
--- a/tests/sys/mac/ipacl/Makefile
+++ b/tests/sys/mac/ipacl/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/mac/ipacl
diff --git a/tests/sys/mac/portacl/Makefile b/tests/sys/mac/portacl/Makefile
index 47993fe54c54..c9fb6bbaae3e 100644
--- a/tests/sys/mac/portacl/Makefile
+++ b/tests/sys/mac/portacl/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/mac/portacl
diff --git a/tests/sys/mqueue/Makefile b/tests/sys/mqueue/Makefile
index 05b5fc97c244..743c82ecc954 100644
--- a/tests/sys/mqueue/Makefile
+++ b/tests/sys/mqueue/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/mqueue
ATF_TESTS_SH= mqueue_test
diff --git a/tests/sys/net/Makefile b/tests/sys/net/Makefile
index 95ab86156a0a..bc8f9c5e9c80 100644
--- a/tests/sys/net/Makefile
+++ b/tests/sys/net/Makefile
@@ -7,6 +7,8 @@ ATF_TESTS_C+= if_epair
ATF_TESTS_SH+= if_epair_test
ATF_TESTS_SH+= if_bridge_test
TEST_METADATA.if_bridge_test+= required_programs="python"
+TEST_METADATA.if_bridge_test+= execenv="jail"
+TEST_METADATA.if_bridge_test+= execenv_jail_params="vnet allow.raw_sockets"
ATF_TESTS_SH+= if_clone_test
ATF_TESTS_SH+= if_gif
ATF_TESTS_SH+= if_lagg_test
@@ -15,6 +17,7 @@ ATF_TESTS_SH+= if_tun_test
ATF_TESTS_SH+= if_vlan
ATF_TESTS_SH+= if_wg
+TESTS_SUBDIRS+= bpf
TESTS_SUBDIRS+= if_ovpn
TESTS_SUBDIRS+= routing
diff --git a/tests/sys/net/bpf/Makefile b/tests/sys/net/bpf/Makefile
new file mode 100644
index 000000000000..9c8a25b15d16
--- /dev/null
+++ b/tests/sys/net/bpf/Makefile
@@ -0,0 +1,15 @@
+.include <src.opts.mk>
+
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/net/bpf
+BINDIR= ${TESTSDIR}
+
+LIBADD+= nv
+
+PROGS= bpf_multi_read
+LIBADD.bpf_multi_read+= pcap
+
+ATF_TESTS_SH= bpf
+
+.include <bsd.test.mk>
diff --git a/tests/sys/net/bpf/bpf.sh b/tests/sys/net/bpf/bpf.sh
new file mode 100644
index 000000000000..2830c4862de9
--- /dev/null
+++ b/tests/sys/net/bpf/bpf.sh
@@ -0,0 +1,67 @@
+##
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC ("Netgate")
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/../../common/vnet.subr
+
+atf_test_case "multi_read" "cleanup"
+multi_read_head()
+{
+ atf_set descr 'Test multiple readers on /dev/bpf'
+ atf_set require.user root
+}
+
+multi_read_body()
+{
+ vnet_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a inet 192.0.2.1/24 up
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b inet 192.0.2.2/24 up
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.2
+
+ # Start a multi-thread (or multi-process) read on bpf
+ $(atf_get_srcdir)/bpf_multi_read ${epair}a &
+
+ # Generate traffic
+ ping -f 192.0.2.2 >/dev/null 2>&1 &
+
+ # Now let this run for 10 seconds
+ sleep 10
+}
+
+multi_read_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "multi_read"
+}
diff --git a/tests/sys/net/bpf/bpf_multi_read.c b/tests/sys/net/bpf/bpf_multi_read.c
new file mode 100644
index 000000000000..3a8edd76d623
--- /dev/null
+++ b/tests/sys/net/bpf/bpf_multi_read.c
@@ -0,0 +1,76 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <pcap.h>
+#include <unistd.h>
+
+static void
+callback(u_char *arg __unused, const struct pcap_pkthdr *hdr __unused,
+ const unsigned char *bytes __unused)
+{
+}
+
+int
+main(int argc, const char **argv)
+{
+ pcap_t *pcap;
+ const char *interface;
+ char errbuf[PCAP_ERRBUF_SIZE] = { 0 };
+ int ret;
+
+ if (argc != 2)
+ err(1, "Usage: %s <interface>\n", argv[0]);
+
+ interface = argv[1];
+
+ pcap = pcap_create(interface, errbuf);
+ if (! pcap)
+ perror("Failed to pcap interface");
+
+ ret = pcap_set_snaplen(pcap, 86);
+ if (ret != 0)
+ perror("Failed to set snaplen");
+
+ ret = pcap_set_timeout(pcap, 100);
+ if (ret != 0)
+ perror("Failed to set timeout");
+
+ ret = pcap_activate(pcap);
+ if (ret != 0)
+ perror("Failed to activate");
+
+ /* So we have two readers on one /dev/bpf fd */
+ fork();
+
+ printf("Interface open\n");
+ pcap_loop(pcap, 0, callback, NULL);
+
+ return (0);
+}
diff --git a/tests/sys/net/if_bridge_test.sh b/tests/sys/net/if_bridge_test.sh
index 44370a905223..cc0b212aebd2 100755
--- a/tests/sys/net/if_bridge_test.sh
+++ b/tests/sys/net/if_bridge_test.sh
@@ -39,6 +39,7 @@ bridge_transmit_ipv4_unicast_head()
bridge_transmit_ipv4_unicast_body()
{
vnet_init
+ vnet_init_bridge
epair_alcatraz=$(vnet_mkepair)
epair_singsing=$(vnet_mkepair)
@@ -76,6 +77,7 @@ stp_head()
stp_body()
{
vnet_init
+ vnet_init_bridge
epair_one=$(vnet_mkepair)
epair_two=$(vnet_mkepair)
@@ -142,6 +144,7 @@ stp_vlan_head()
stp_vlan_body()
{
vnet_init
+ vnet_init_bridge
epair_one=$(vnet_mkepair)
epair_two=$(vnet_mkepair)
@@ -216,6 +219,7 @@ static_head()
static_body()
{
vnet_init
+ vnet_init_bridge
epair=$(vnet_mkepair)
bridge=$(vnet_mkbridge)
@@ -267,12 +271,13 @@ span_head()
{
atf_set descr 'Bridge span test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
span_body()
{
vnet_init
+ vnet_init_bridge
epair=$(vnet_mkepair)
epair_span=$(vnet_mkepair)
@@ -326,6 +331,7 @@ delete_with_members_head()
delete_with_members_body()
{
vnet_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair=$(vnet_mkepair)
@@ -352,6 +358,7 @@ mac_conflict_head()
mac_conflict_body()
{
vnet_init
+ vnet_init_bridge
epair=$(vnet_mkepair)
@@ -390,6 +397,7 @@ inherit_mac_head()
inherit_mac_body()
{
vnet_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair=$(vnet_mkepair)
@@ -412,12 +420,13 @@ stp_validation_head()
{
atf_set descr 'Check STP validation'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
stp_validation_body()
{
vnet_init
+ vnet_init_bridge
epair_one=$(vnet_mkepair)
epair_two=$(vnet_mkepair)
@@ -461,6 +470,7 @@ gif_head()
gif_body()
{
vnet_init
+ vnet_init_bridge
epair=$(vnet_mkepair)
@@ -545,6 +555,7 @@ check_mtu()
mtu_body()
{
vnet_init
+ vnet_init_bridge
epair=$(vnet_mkepair)
gif=$(ifconfig gif create)
@@ -606,6 +617,7 @@ vlan_head()
vlan_body()
{
vnet_init
+ vnet_init_bridge
vid=1
@@ -673,6 +685,7 @@ many_bridge_members_head()
many_bridge_members_body()
{
vnet_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
ifcount=256
@@ -690,6 +703,524 @@ many_bridge_members_cleanup()
vnet_cleanup
}
+atf_test_case "member_ifaddrs_enabled" "cleanup"
+member_ifaddrs_enabled_head()
+{
+ atf_set descr 'bridge with member_ifaddrs=1'
+ atf_set require.user root
+}
+
+member_ifaddrs_enabled_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ ep=$(vnet_mkepair)
+ ifconfig ${ep}a inet 192.0.2.1/24 up
+
+ vnet_mkjail one ${ep}b
+ jexec one sysctl net.link.bridge.member_ifaddrs=1
+ jexec one ifconfig ${ep}b inet 192.0.2.2/24 up
+ jexec one ifconfig bridge0 create addm ${ep}b
+
+ atf_check -s exit:0 -o ignore ping -c3 -t1 192.0.2.2
+}
+
+member_ifaddrs_enabled_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "member_ifaddrs_disabled" "cleanup"
+member_ifaddrs_disabled_head()
+{
+ atf_set descr 'bridge with member_ifaddrs=0'
+ atf_set require.user root
+}
+
+member_ifaddrs_disabled_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ vnet_mkjail one
+ jexec one sysctl net.link.bridge.member_ifaddrs=0
+
+ bridge=$(jexec one ifconfig bridge create)
+
+ # adding an interface with an IPv4 address
+ ep=$(jexec one ifconfig epair create)
+ jexec one ifconfig ${ep} 192.0.2.1/32
+ atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep}
+
+ # adding an interface with an IPv6 address
+ ep=$(jexec one ifconfig epair create)
+ jexec one ifconfig ${ep} inet6 2001:db8::1/128
+ atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep}
+
+ # adding an interface with an IPv6 link-local address
+ ep=$(jexec one ifconfig epair create)
+ jexec one ifconfig ${ep} inet6 -ifdisabled auto_linklocal up
+ atf_check -s exit:1 -e ignore jexec one ifconfig ${bridge} addm ${ep}
+
+ # adding an IPv4 address to a member
+ ep=$(jexec one ifconfig epair create)
+ jexec one ifconfig ${bridge} addm ${ep}
+ atf_check -s exit:1 -e ignore jexec one ifconfig ${ep} inet 192.0.2.2/32
+
+ # adding an IPv6 address to a member
+ ep=$(jexec one ifconfig epair create)
+ jexec one ifconfig ${bridge} addm ${ep}
+ atf_check -s exit:1 -e ignore jexec one ifconfig ${ep} inet6 2001:db8::1/128
+}
+
+member_ifaddrs_disabled_cleanup()
+{
+ vnet_cleanup
+}
+
+#
+# Test kern/287150: when member_ifaddrs=0, and a physical interface which is in
+# a bridge also has a vlan(4) on it, tagged packets are not correctly passed to
+# vlan(4).
+atf_test_case "member_ifaddrs_vlan" "cleanup"
+member_ifaddrs_vlan_head()
+{
+ atf_set descr 'kern/287150: vlan and bridge on the same interface'
+ atf_set require.user root
+}
+
+member_ifaddrs_vlan_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ # The first jail has an epair with an IP address on vlan 20.
+ vnet_mkjail one ${epone}a
+ atf_check -s exit:0 jexec one ifconfig ${epone}a up
+ atf_check -s exit:0 jexec one \
+ ifconfig ${epone}a.20 create inet 192.0.2.1/24 up
+
+ # The second jail has an epair with an IP address on vlan 20,
+ # which is also in a bridge.
+ vnet_mkjail two ${epone}b
+
+ jexec two ifconfig
+ atf_check -s exit:0 -o save:bridge jexec two ifconfig bridge create
+ bridge=$(cat bridge)
+ atf_check -s exit:0 jexec two ifconfig ${bridge} addm ${epone}b up
+
+ atf_check -s exit:0 -o ignore jexec two \
+ sysctl net.link.bridge.member_ifaddrs=0
+ atf_check -s exit:0 jexec two ifconfig ${epone}b up
+ atf_check -s exit:0 jexec two \
+ ifconfig ${epone}b.20 create inet 192.0.2.2/24 up
+
+ # Make sure the two jails can communicate over the vlan.
+ atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+member_ifaddrs_vlan_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "vlan_pvid" "cleanup"
+vlan_pvid_head()
+{
+ atf_set descr 'bridge with two ports with pvid set'
+ atf_set require.user root
+}
+
+vlan_pvid_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ jexec one ifconfig ${epone}b 192.0.2.1/24 up
+ jexec two ifconfig ${eptwo}b 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+ ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
+ ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20
+
+ # With VLAN filtering enabled, traffic should be passed.
+ atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+ # Removed the untagged VLAN on one port; traffic should not be passed.
+ ifconfig ${bridge} -untagged ${epone}a
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_pvid_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "vlan_pvid_filtered" "cleanup"
+vlan_pvid_filtered_head()
+{
+ atf_set descr 'bridge with two ports with different pvids'
+ atf_set require.user root
+}
+
+vlan_pvid_filtered_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ jexec one ifconfig ${epone}b 192.0.2.1/24 up
+ jexec two ifconfig ${eptwo}b 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+ ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
+ ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 30
+
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_pvid_filtered_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "vlan_pvid_tagged" "cleanup"
+vlan_pvid_tagged_head()
+{
+ atf_set descr 'bridge pvid with tagged frames for pvid'
+ atf_set require.user root
+}
+
+vlan_pvid_tagged_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ # Create two tagged interfaces on the appropriate VLANs
+ jexec one ifconfig ${epone}b up
+ jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up
+ jexec two ifconfig ${eptwo}b up
+ jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+ ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
+ ifconfig ${bridge} addm ${eptwo}a untagged ${eptwo}a 20
+
+ # Tagged frames should not be passed.
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_pvid_tagged_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "vlan_pvid_1q" "cleanup"
+vlan_pvid_1q_head()
+{
+ atf_set descr '802.1q tag addition and removal'
+ atf_set require.user root
+}
+
+vlan_pvid_1q_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ # Set up one jail with an access port, and the other with a trunk port.
+ # This forces the bridge to add and remove .1q tags to bridge the
+ # traffic.
+
+ jexec one ifconfig ${epone}b 192.0.2.1/24 up
+ jexec two ifconfig ${eptwo}b up
+ jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} addm ${epone}a untagged ${epone}a 20
+ ifconfig ${bridge} addm ${eptwo}a
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+
+ atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_pvid_1q_cleanup()
+{
+ vnet_cleanup
+}
+
+#
+# Test vlan filtering.
+#
+atf_test_case "vlan_filtering" "cleanup"
+vlan_filtering_head()
+{
+ atf_set descr 'tagged traffic with filtering'
+ atf_set require.user root
+}
+
+vlan_filtering_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ jexec one ifconfig ${epone}b up
+ jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up
+ jexec two ifconfig ${eptwo}b up
+ jexec two ifconfig ${eptwo}b.20 create 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+ ifconfig ${bridge} addm ${epone}a vlanfilter ${epone}a
+ ifconfig ${bridge} addm ${eptwo}a vlanfilter ${eptwo}a
+
+ # Right now there are no VLANs on the access list, so everything
+ # should be blocked.
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+ # Set the untagged vlan on both ports to 20 and make sure traffic is
+ # still blocked. We intentionally do not pass tagged traffic for the
+ # untagged vlan.
+ atf_check -s exit:0 ifconfig ${bridge} untagged ${epone}a 20
+ atf_check -s exit:0 ifconfig ${bridge} untagged ${eptwo}a 20
+
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+ atf_check -s exit:0 ifconfig ${bridge} -untagged ${epone}a
+ atf_check -s exit:0 ifconfig ${bridge} -untagged ${eptwo}a
+
+ # Add VLANs 10-30 to the access list; now access should be allowed.
+ ifconfig ${bridge} +tagged ${epone}a 10-30
+ ifconfig ${bridge} +tagged ${eptwo}a 10-30
+ atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+ # Remove vlan 20 from the access list, now access should be blocked
+ # again.
+ ifconfig ${bridge} -tagged ${epone}a 20
+ ifconfig ${bridge} -tagged ${eptwo}a 20
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_filtering_cleanup()
+{
+ vnet_cleanup
+}
+
+#
+# Test the ifconfig 'tagged' option.
+#
+atf_test_case "vlan_ifconfig_tagged" "cleanup"
+vlan_ifconfig_tagged_head()
+{
+ atf_set descr 'test the ifconfig tagged option'
+ atf_set require.user root
+}
+
+vlan_ifconfig_tagged_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ ep=$(vnet_mkepair)
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} addm ${ep}a vlanfilter ${ep}a up
+ ifconfig ${ep}a up
+
+ # To start with, no vlans should be configured.
+ atf_check -s exit:0 -o not-match:"tagged" ifconfig ${bridge}
+
+ # Add vlans 100-149.
+ atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a 100-149
+ atf_check -s exit:0 -o match:"tagged 100-149" ifconfig ${bridge}
+
+ # Replace the vlan list with 139-199.
+ atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a 139-199
+ atf_check -s exit:0 -o match:"tagged 139-199" ifconfig ${bridge}
+
+ # Add vlans 100-170.
+ atf_check -s exit:0 ifconfig ${bridge} +tagged ${ep}a 100-170
+ atf_check -s exit:0 -o match:"tagged 100-199" ifconfig ${bridge}
+
+ # Remove vlans 104, 105, and 150-159
+ atf_check -s exit:0 ifconfig ${bridge} -tagged ${ep}a 104,105,150-159
+ atf_check -s exit:0 -o match:"tagged 100-103,106-149,160-199" \
+ ifconfig ${bridge}
+
+ # Remove the entire vlan list.
+ atf_check -s exit:0 ifconfig ${bridge} tagged ${ep}a none
+ atf_check -s exit:0 -o not-match:"tagged" ifconfig ${bridge}
+
+ # Test some invalid vlans sets.
+ for bad_vlan in -1 0 4096 4097 foo 0-10 4000-5000 foo-40 40-foo; do
+ atf_check -s exit:1 -e ignore \
+ ifconfig ${bridge} tagged "$bad_vlan"
+ done
+}
+
+vlan_ifconfig_tagged_cleanup()
+{
+ vnet_cleanup
+}
+
+#
+# Test a vlan(4) "SVI" interface on top of a bridge.
+#
+atf_test_case "vlan_svi" "cleanup"
+vlan_svi_head()
+{
+ atf_set descr 'vlan bridge with an SVI'
+ atf_set require.user root
+}
+
+vlan_svi_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+
+ jexec one ifconfig ${epone}b up
+ jexec one ifconfig ${epone}b.20 create 192.0.2.1/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${bridge} addm ${epone}a tagged ${epone}a 20
+
+ svi=$(vnet_mkvlan)
+ ifconfig ${svi} vlan 20 vlandev ${bridge}
+ ifconfig ${svi} inet 192.0.2.2/24 up
+
+ atf_check -s exit:0 -o ignore ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_svi_cleanup()
+{
+ vnet_cleanup
+}
+
+#
+# Test QinQ (802.1ad).
+#
+atf_test_case "vlan_qinq" "cleanup"
+vlan_qinq_head()
+{
+ atf_set descr 'vlan filtering with QinQ traffic'
+ atf_set require.user root
+}
+
+vlan_qinq_body()
+{
+ vnet_init
+ vnet_init_bridge
+
+ epone=$(vnet_mkepair)
+ eptwo=$(vnet_mkepair)
+
+ vnet_mkjail one ${epone}b
+ vnet_mkjail two ${eptwo}b
+
+ # Create a QinQ trunk between the two jails. The outer (provider) tag
+ # is 5, and the inner tag is 10.
+
+ jexec one ifconfig ${epone}b up
+ jexec one ifconfig ${epone}b.5 create vlanproto 802.1ad up
+ jexec one ifconfig ${epone}b.5.10 create inet 192.0.2.1/24 up
+
+ jexec two ifconfig ${eptwo}b up
+ jexec two ifconfig ${eptwo}b.5 create vlanproto 802.1ad up
+ jexec two ifconfig ${eptwo}b.5.10 create inet 192.0.2.2/24 up
+
+ bridge=$(vnet_mkbridge)
+
+ ifconfig ${bridge} up
+ ifconfig ${epone}a up
+ ifconfig ${eptwo}a up
+ ifconfig ${bridge} addm ${epone}a vlanfilter ${epone}a
+ ifconfig ${bridge} addm ${eptwo}a vlanfilter ${eptwo}a
+
+ # Right now there are no VLANs on the access list, so everything
+ # should be blocked.
+ atf_check -s exit:2 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:2 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+
+ # Add the provider tag to the access list; now traffic should be passed.
+ ifconfig ${bridge} +tagged ${epone}a 5
+ ifconfig ${bridge} +tagged ${eptwo}a 5
+ atf_check -s exit:0 -o ignore jexec one ping -c 3 -t 1 192.0.2.2
+ atf_check -s exit:0 -o ignore jexec two ping -c 3 -t 1 192.0.2.1
+}
+
+vlan_qinq_cleanup()
+{
+ vnet_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "bridge_transmit_ipv4_unicast"
@@ -705,4 +1236,15 @@ atf_init_test_cases()
atf_add_test_case "mtu"
atf_add_test_case "vlan"
atf_add_test_case "many_bridge_members"
+ atf_add_test_case "member_ifaddrs_enabled"
+ atf_add_test_case "member_ifaddrs_disabled"
+ atf_add_test_case "member_ifaddrs_vlan"
+ atf_add_test_case "vlan_pvid"
+ atf_add_test_case "vlan_pvid_1q"
+ atf_add_test_case "vlan_pvid_filtered"
+ atf_add_test_case "vlan_pvid_tagged"
+ atf_add_test_case "vlan_filtering"
+ atf_add_test_case "vlan_ifconfig_tagged"
+ atf_add_test_case "vlan_svi"
+ atf_add_test_case "vlan_qinq"
}
diff --git a/tests/sys/net/if_lagg_test.sh b/tests/sys/net/if_lagg_test.sh
index 6b99aaedfbbf..e2b998599991 100755
--- a/tests/sys/net/if_lagg_test.sh
+++ b/tests/sys/net/if_lagg_test.sh
@@ -83,10 +83,6 @@ status_stress_body()
{
local TAP0 TAP1 LAGG MAC
- if [ "$(atf_config_get ci false)" = "true" ]; then
- atf_skip "Skipping this test because it panics the machine fairly often"
- fi
-
# Configure the lagg interface to use an RFC5737 nonrouteable addresses
ADDR="192.0.2.2"
MASK="24"
@@ -142,8 +138,6 @@ create_destroy_stress_body()
{
local TAP0 TAP1 LAGG MAC
- atf_skip "Skipping this test because it easily panics the machine"
-
TAP0=`get_tap`
TAP1=`get_tap`
TAP2=`get_tap`
@@ -196,10 +190,6 @@ lacp_linkstate_destroy_stress_head()
}
lacp_linkstate_destroy_stress_body()
{
- if [ "$(atf_config_get ci false)" = "true" ]; then
- atf_skip "https://bugs.freebsd.org/244168"
- fi
-
local TAP0 TAP1 LAGG MAC SRCDIR
# Configure the lagg interface to use an RFC5737 nonrouteable addresses
@@ -261,8 +251,6 @@ up_destroy_stress_body()
{
local TAP0 TAP1 LAGG MAC SRCDIR
- atf_skip "Skipping this test because it panics the machine fairly often"
-
# Configure the lagg interface to use an RFC5737 nonrouteable addresses
ADDR="192.0.2.2"
MASK="24"
@@ -358,7 +346,6 @@ updown_body()
{
local TAP0 TAP1 LAGG MAC
- atf_expect_fail "PR 226144 Upping a lagg interrface should automatically up its children"
# Configure the lagg interface to use an RFC5737 nonrouteable addresses
ADDR="192.0.2.2"
MASK="24"
diff --git a/tests/sys/net/if_ovpn/Makefile b/tests/sys/net/if_ovpn/Makefile
index 823ad06e76ee..85746226e122 100644
--- a/tests/sys/net/if_ovpn/Makefile
+++ b/tests/sys/net/if_ovpn/Makefile
@@ -6,8 +6,8 @@ TESTSDIR= ${TESTSBASE}/sys/net/if_ovpn
.if ${MK_PF} != "no"
ATF_TESTS_SH+= if_ovpn
-# Tests reuse jail names and so cannot run in parallel.
-TEST_METADATA.if_ovpn+= is_exclusive=true
+TEST_METADATA.if_ovpn+= execenv="jail"
+TEST_METADATA.if_ovpn+= execenv_jail_params="vnet allow.raw_sockets"
.endif
ATF_TESTS_C+= if_ovpn_c
diff --git a/tests/sys/net/if_ovpn/if_ovpn.sh b/tests/sys/net/if_ovpn/if_ovpn.sh
index 0ec2563cf355..2138e0f666ec 100644
--- a/tests/sys/net/if_ovpn/if_ovpn.sh
+++ b/tests/sys/net/if_ovpn/if_ovpn.sh
@@ -95,6 +95,10 @@ atf_test_case "4in4" "cleanup"
echo 'foo' | jexec b nc -u -w 2 192.0.2.1 1194
atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1
+
+ # Test routing loop protection
+ jexec b route add 192.0.2.1 198.51.100.1
+ atf_check -s exit:2 -o ignore jexec b ping -t 1 -c 1 198.51.100.1
}
4in4_cleanup()
@@ -102,6 +106,86 @@ atf_test_case "4in4" "cleanup"
ovpn_cleanup
}
+atf_test_case "bz283426" "cleanup"
+bz283426_head()
+{
+ atf_set descr 'FreeBSD Bugzilla 283426'
+ atf_set require.user root
+ atf_set require.progs openvpn python3
+}
+
+bz283426_body()
+{
+ ovpn_init
+
+ l=$(vnet_mkepair)
+
+ vnet_mkjail a ${l}a
+ jexec a ifconfig ${l}a 192.0.2.1/24 up
+ vnet_mkjail b ${l}b
+ jexec b ifconfig ${l}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore jexec a ping -c 1 192.0.2.2
+
+ ovpn_start a "
+ dev ovpn0
+ dev-type tun
+ proto udp4
+
+ cipher AES-256-GCM
+ auth SHA256
+
+ bind 0.0.0.0:1194
+ server 198.51.100.0 255.255.255.0
+ ca $(atf_get_srcdir)/ca.crt
+ cert $(atf_get_srcdir)/server.crt
+ key $(atf_get_srcdir)/server.key
+ dh $(atf_get_srcdir)/dh.pem
+
+ mode server
+ script-security 2
+ auth-user-pass-verify /usr/bin/true via-env
+ topology subnet
+
+ keepalive 100 600
+ "
+ ovpn_start b "
+ dev tun0
+ dev-type tun
+
+ client
+
+ remote 192.0.2.1
+ auth-user-pass $(atf_get_srcdir)/user.pass
+
+ ca $(atf_get_srcdir)/ca.crt
+ cert $(atf_get_srcdir)/client.crt
+ key $(atf_get_srcdir)/client.key
+ dh $(atf_get_srcdir)/dh.pem
+
+ keepalive 100 600
+ "
+
+ # Give the tunnel time to come up
+ sleep 10
+
+ atf_check -s exit:0 -o ignore jexec b ping -c 1 198.51.100.1
+
+ # Send a broadcast packet in the outer link.
+ echo "import socket as sk
+s = sk.socket(sk.AF_INET, sk.SOCK_DGRAM)
+s.setsockopt(sk.SOL_SOCKET, sk.SO_BROADCAST, 1)
+s.sendto(b'x' * 1000, ('192.0.2.255', 1194))" | jexec b python3
+
+ atf_check -s exit:0 -o ignore jexec b ping -c 3 198.51.100.1
+}
+
+bz283426_cleanup()
+{
+ ovpn_cleanup
+}
+
atf_test_case "4mapped" "cleanup"
4mapped_head()
{
@@ -404,6 +488,10 @@ atf_test_case "6in6" "cleanup"
atf_check -s exit:0 -o ignore jexec b ping6 -c 3 2001:db8:1::1
atf_check -s exit:0 -o ignore jexec b ping6 -c 3 -z 16 2001:db8:1::1
+
+ # Test routing loop protection
+ jexec b route add -6 2001:db8::1 2001:db8:1::1
+ atf_check -s exit:2 -o ignore jexec b ping6 -t 1 -c 3 2001:db8:1::1
}
6in6_cleanup()
@@ -594,6 +682,7 @@ multi_client_head()
multi_client_body()
{
ovpn_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
srv=$(vnet_mkepair)
@@ -806,6 +895,7 @@ ra_head()
ra_body()
{
ovpn_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
srv=$(vnet_mkepair)
@@ -1038,9 +1128,31 @@ gcm_128_cleanup()
ovpn_cleanup
}
+atf_test_case "destroy_unused" "cleanup"
+destroy_unused_head()
+{
+ atf_set descr 'Destroy an if_ovpn interface before it is used'
+ atf_set require.user root
+}
+
+destroy_unused_body()
+{
+ ovpn_init
+
+ intf=$(ifconfig ovpn create)
+ atf_check -s exit:0 \
+ ifconfig ${intf} destroy
+}
+
+destroy_unused_cleanup()
+{
+ ovpn_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "4in4"
+ atf_add_test_case "bz283426"
atf_add_test_case "4mapped"
atf_add_test_case "6in4"
atf_add_test_case "6in6"
@@ -1052,4 +1164,5 @@ atf_init_test_cases()
atf_add_test_case "ra"
atf_add_test_case "chacha"
atf_add_test_case "gcm_128"
+ atf_add_test_case "destroy_unused"
}
diff --git a/tests/sys/net/if_vlan.sh b/tests/sys/net/if_vlan.sh
index 675ed0090e8c..424eac705b94 100755
--- a/tests/sys/net/if_vlan.sh
+++ b/tests/sys/net/if_vlan.sh
@@ -22,8 +22,12 @@ basic_body()
jexec alcatraz ifconfig ${epair_vlan}a up
jexec alcatraz ifconfig ${vlan0} 10.0.0.1/24 up
- vlan1=$(jexec singsing ifconfig vlan create vlandev ${epair_vlan}b \
- vlan 42)
+ vlan1=$(jexec singsing ifconfig vlan create)
+
+ # Test associating the physical interface
+ atf_check -s exit:0 \
+ jexec singsing ifconfig ${vlan1} vlandev ${epair_vlan}b vlan 42
+
jexec singsing ifconfig ${epair_vlan}b up
jexec singsing ifconfig ${vlan1} 10.0.0.2/24 up
@@ -37,7 +41,7 @@ basic_body()
# And change back
# Test changing the vlan ID
atf_check -s exit:0 \
- jexec singsing ifconfig ${vlan1} vlandev ${epair_vlan}b vlan 42
+ jexec singsing ifconfig ${vlan1} vlan 42 vlandev ${epair_vlan}b
atf_check -s exit:0 -o ignore jexec singsing ping -c 1 10.0.0.1
}
@@ -253,7 +257,7 @@ bpf_pcp_head()
{
atf_set descr 'Set VLAN PCP through BPF'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
bpf_pcp_body()
@@ -293,6 +297,42 @@ bpf_pcp_cleanup()
vnet_cleanup
}
+atf_test_case "conflict_id" "cleanup"
+conflict_id_head()
+{
+ atf_set descr 'Test conflicting VLAN IDs, PR #279195'
+ atf_set require.user root
+}
+
+conflict_id_body()
+{
+ vnet_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair}b
+ vlan_a=$(jexec alcatraz ifconfig vlan create)
+ vlan_b=$(jexec alcatraz ifconfig vlan create)
+
+ jexec alcatraz ifconfig ${vlan_a} vlan 100 vlandev ${epair}b
+ jexec alcatraz ifconfig ${vlan_b} vlan 101 vlandev ${epair}b
+
+ atf_check -s exit:1 -o ignore -e ignore \
+ jexec alcatraz ifconfig ${vlan_a} vlan 101
+
+ atf_check -s exit:0 -o match:"vlan: 100" \
+ jexec alcatraz ifconfig ${vlan_a}
+
+ atf_check -s exit:0 -o ignore -e ignore \
+ jexec alcatraz ifconfig ${vlan_a} vlan 100
+}
+
+conflict_id_cleanup()
+{
+ vnet_cleanup
+
+}
+
atf_init_test_cases()
{
atf_add_test_case "basic"
@@ -302,4 +342,5 @@ atf_init_test_cases()
atf_add_test_case "qinq_dot"
atf_add_test_case "qinq_setflags"
atf_add_test_case "bpf_pcp"
+ atf_add_test_case "conflict_id"
}
diff --git a/tests/sys/net/if_wg.sh b/tests/sys/net/if_wg.sh
index b43b40f25018..1f51d86c8efa 100644
--- a/tests/sys/net/if_wg.sh
+++ b/tests/sys/net/if_wg.sh
@@ -34,6 +34,7 @@ wg_basic_head()
{
atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails'
atf_set require.user root
+ atf_set require.kmods if_wg
}
wg_basic_body()
@@ -41,8 +42,6 @@ wg_basic_body()
local epair pri1 pri2 pub1 pub2 wg1 wg2
local endpoint1 endpoint2 tunnel1 tunnel2
- kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
-
pri1=$(wg genkey)
pri2=$(wg genkey)
@@ -92,11 +91,90 @@ wg_basic_cleanup()
vnet_cleanup
}
+atf_test_case "wg_basic_crossaf" "cleanup"
+wg_basic_crossaf_head()
+{
+ atf_set descr 'Create a wg(4) tunnel and pass IPv4 traffic over an IPv6 nexthop'
+ atf_set require.user root
+}
+
+wg_basic_crossaf_body()
+{
+ local epair pri1 pri2 pub1 pub2 wg1 wg2
+ local endpoint1 endpoint2 tunnel1 tunnel2
+ local testnet testlocal testremote
+
+ kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
+
+ pri1=$(wg genkey)
+ pri2=$(wg genkey)
+
+ endpoint1=192.168.2.1
+ endpoint2=192.168.2.2
+ tunnel1=2001:db8:1::1
+ tunnel2=2001:db8:1::2
+
+ testnet=192.168.3.0/24
+ testlocal=192.168.3.1
+ testremote=192.168.3.2
+
+ epair=$(vnet_mkepair)
+
+ vnet_init
+
+ vnet_mkjail wgtest1 ${epair}a
+ vnet_mkjail wgtest2 ${epair}b
+
+ jexec wgtest1 ifconfig ${epair}a ${endpoint1}/24 up
+ jexec wgtest2 ifconfig ${epair}b ${endpoint2}/24 up
+
+ wg1=$(jexec wgtest1 ifconfig wg create)
+ echo "$pri1" | jexec wgtest1 wg set $wg1 listen-port 12345 \
+ private-key /dev/stdin
+ pub1=$(jexec wgtest1 wg show $wg1 public-key)
+ wg2=$(jexec wgtest2 ifconfig wg create)
+ echo "$pri2" | jexec wgtest2 wg set $wg2 listen-port 12345 \
+ private-key /dev/stdin
+ pub2=$(jexec wgtest2 wg show $wg2 public-key)
+
+ atf_check -s exit:0 -o ignore \
+ jexec wgtest1 wg set $wg1 peer "$pub2" \
+ endpoint ${endpoint2}:12345 allowed-ips ${tunnel2}/128,${testnet}
+ atf_check -s exit:0 \
+ jexec wgtest1 ifconfig $wg1 inet6 ${tunnel1}/64 up
+
+ atf_check -s exit:0 -o ignore \
+ jexec wgtest2 wg set $wg2 peer "$pub1" \
+ endpoint ${endpoint1}:12345 allowed-ips ${tunnel1}/128,${testnet}
+ atf_check -s exit:0 \
+ jexec wgtest2 ifconfig $wg2 inet6 ${tunnel2}/64 up
+
+ atf_check -s exit:0 jexec wgtest1 ifconfig $wg1 inet ${testlocal}/32
+ atf_check -s exit:0 jexec wgtest2 ifconfig $wg2 inet ${testremote}/32
+
+ # Generous timeout since the handshake takes some time.
+ atf_check -s exit:0 -o ignore jexec wgtest1 ping -c 1 -t 5 "$tunnel2"
+
+ # Setup our IPv6 endpoint and routing
+ atf_check -s exit:0 -o ignore \
+ jexec wgtest1 route add -inet ${testnet} -inet6 "$tunnel2"
+ atf_check -s exit:0 -o ignore \
+ jexec wgtest2 route add -inet ${testnet} -inet6 "$tunnel1"
+ # Now ping an address on the other side
+ atf_check -s exit:0 -o ignore jexec wgtest1 ping -c 1 -t 3 ${testremote}
+}
+
+wg_basic_crossaf_cleanup()
+{
+ vnet_cleanup
+}
+
atf_test_case "wg_basic_netmap" "cleanup"
wg_basic_netmap_head()
{
atf_set descr 'Create a wg(4) tunnel over an epair and pass traffic between jails with netmap'
atf_set require.user root
+ atf_set require.kmods if_wg netmap
}
wg_basic_netmap_body()
@@ -105,9 +183,6 @@ wg_basic_netmap_body()
local endpoint1 endpoint2 tunnel1 tunnel2 tunnel3 tunnel4
local pid status
- kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
- kldload -n netmap || atf_skip "This test requires netmap and could not load it"
-
pri1=$(wg genkey)
pri2=$(wg genkey)
@@ -190,6 +265,7 @@ wg_key_peerdev_shared_head()
{
atf_set descr 'Create a wg(4) interface with a shared pubkey between device and a peer'
atf_set require.user root
+ atf_set require.kmods if_wg
}
wg_key_peerdev_shared_body()
@@ -197,8 +273,6 @@ wg_key_peerdev_shared_body()
local epair pri1 pub1 wg1
local endpoint1 tunnel1
- kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
-
pri1=$(wg genkey)
endpoint1=192.168.2.1
@@ -238,8 +312,6 @@ wg_key_peerdev_makeshared_body()
local epair pri1 pub1 pri2 wg1 wg2
local endpoint1 tunnel1
- kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
-
pri1=$(wg genkey)
pri2=$(wg genkey)
@@ -283,6 +355,7 @@ wg_vnet_parent_routing_head()
{
atf_set descr 'Create a wg(4) tunnel without epairs and pass traffic between jails'
atf_set require.user root
+ atf_set require.kmods if_wg
}
wg_vnet_parent_routing_body()
@@ -290,8 +363,6 @@ wg_vnet_parent_routing_body()
local pri1 pri2 pub1 pub2 wg1 wg2
local tunnel1 tunnel2
- kldload -n if_wg
-
pri1=$(wg genkey)
pri2=$(wg genkey)
@@ -346,11 +417,217 @@ wg_vnet_parent_routing_cleanup()
vnet_cleanup
}
+# The kernel should now allow removing a single allowed-ip without having to
+# replace the whole list. We can't really test the atomicity of it all that
+# easily, but we'll trust that it worked right if just that addr/mask is gone.
+atf_test_case "wg_allowedip_incremental" "cleanup"
+wg_allowedip_incremental_head()
+{
+ atf_set descr "Add/remove allowed-ips from a peer with the +/- incremental syntax"
+ atf_set require.user root
+}
+
+wg_allowedip_incremental_body()
+{
+ local pri1 pri2 pub1 pub2 wg1
+ local tunnel1 tunnel2 tunnel3
+
+ kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
+
+ pri1=$(wg genkey)
+ pri2=$(wg genkey)
+ pub2=$(echo "$pri2" | wg pubkey)
+
+ tunnel1=169.254.0.1
+ tunnel2=169.254.0.2
+ tunnel3=169.254.0.3
+
+ vnet_mkjail wgtest1
+
+ wg1=$(jexec wgtest1 ifconfig wg create)
+ echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin
+ pub1=$(jexec wgtest1 wg show $wg1 public-key)
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "${tunnel1}/32,${tunnel2}/32"
+
+ atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -q "${tunnel1}/32" wg.allowed
+ atf_check grep -q "${tunnel2}/32" wg.allowed
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "-${tunnel2}/32"
+
+ atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -q "${tunnel1}/32" wg-2.allowed
+ atf_check -s not-exit:0 grep -q "${tunnel2}/32" wg-2.allowed
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "+${tunnel2}/32"
+
+ atf_check -o save:wg-3.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -q "${tunnel1}/32" wg-3.allowed
+ atf_check grep -q "${tunnel2}/32" wg-3.allowed
+
+ # Now attempt to add the address yet again to confirm that it's not
+ # harmful.
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "+${tunnel2}/32"
+
+ atf_check -o save:wg-4.allowed -x \
+ "jexec wgtest1 wg show $wg1 allowed-ips | cut -f2 | tr ' ' '\n'"
+ atf_check -o match:"2 wg-4.allowed$" wc -l wg-4.allowed
+
+ # Finally, let's try removing an address that we never had at all and
+ # confirm that we still have our two addresses.
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "-${tunnel3}/32"
+
+ atf_check -o save:wg-5.allowed -x \
+ "jexec wgtest1 wg show $wg1 allowed-ips | cut -f2 | tr ' ' '\n'"
+ atf_check cmp -s wg-4.allowed wg-5.allowed
+}
+
+wg_allowedip_incremental_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "wg_allowedip_incremental_inet6" "cleanup"
+wg_allowedip_incremental_inet6_head()
+{
+ atf_set descr "Add/remove IPv6 allowed-ips from a peer with the +/- incremental syntax"
+ atf_set require.user root
+}
+
+wg_allowedip_incremental_inet6_body()
+{
+ local pri1 pri2 pub1 pub2 wg1
+ local tunnel1 tunnel2
+
+ kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
+
+ pri1=$(wg genkey)
+ pri2=$(wg genkey)
+ pub2=$(echo "$pri2" | wg pubkey)
+
+ tunnel1=2001:db8:1::1
+ tunnel2=2001:db8:1::2
+
+ vnet_mkjail wgtest1
+
+ wg1=$(jexec wgtest1 ifconfig wg create)
+ echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin
+ pub1=$(jexec wgtest1 wg show $wg1 public-key)
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "${tunnel1}/128"
+ atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -q "${tunnel1}/128" wg.allowed
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "+${tunnel2}/128"
+ atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -q "${tunnel1}/128" wg-2.allowed
+ atf_check grep -q "${tunnel2}/128" wg-2.allowed
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "-${tunnel1}/128"
+ atf_check -o save:wg-3.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check -s not-exit:0 grep -q "${tunnel1}/128" wg-3.allowed
+ atf_check grep -q "${tunnel2}/128" wg-3.allowed
+}
+
+wg_allowedip_incremental_inet6_cleanup()
+{
+ vnet_cleanup
+}
+
+
+atf_test_case "wg_allowedip_incremental_stealing" "cleanup"
+wg_allowedip_incremental_stealing_head()
+{
+ atf_set descr "Add/remove allowed-ips from a peer with the +/- incremental syntax to steal"
+ atf_set require.user root
+}
+
+wg_allowedip_incremental_stealing_body()
+{
+ local pri1 pri2 pri3 pub1 pub2 pub3 wg1
+ local regex2 regex3
+ local tunnel1 tunnel2
+
+ kldload -n if_wg || atf_skip "This test requires if_wg and could not load it"
+
+ pri1=$(wg genkey)
+ pri2=$(wg genkey)
+ pri3=$(wg genkey)
+ pub2=$(echo "$pri2" | wg pubkey)
+ pub3=$(echo "$pri3" | wg pubkey)
+
+ regex2=$(echo "$pub2" | sed -e 's/[+]/[+]/g')
+ regex3=$(echo "$pub3" | sed -e 's/[+]/[+]/g')
+
+ tunnel1=169.254.0.1
+ tunnel2=169.254.0.2
+ tunnel3=169.254.0.3
+
+ vnet_mkjail wgtest1
+
+ wg1=$(jexec wgtest1 ifconfig wg create)
+ echo "$pri1" | jexec wgtest1 wg set $wg1 private-key /dev/stdin
+ pub1=$(jexec wgtest1 wg show $wg1 public-key)
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "${tunnel1}/32,${tunnel2}/32"
+
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub3 \
+ allowed-ips "${tunnel3}/32"
+
+ # First, confirm that the negative syntax doesn't do anything because
+ # we have the wrong peer.
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "-${tunnel3}/32"
+
+ atf_check -o save:wg.allowed jexec wgtest1 wg show $wg1 allowed-ips
+ atf_check grep -Eq "^${regex3}.+${tunnel3}/32" wg.allowed
+
+ # Next, steal it with an incremental move and check that it moved.
+ atf_check -s exit:0 \
+ jexec wgtest1 wg set $wg1 peer $pub2 \
+ allowed-ips "+${tunnel3}/32"
+
+ atf_check -o save:wg-2.allowed jexec wgtest1 wg show $wg1 allowed-ips
+
+ atf_check grep -Eq "^${regex2}.+${tunnel3}/32" wg-2.allowed
+ atf_check grep -Evq "^${regex3}.+${tunnel3}/32" wg-2.allowed
+}
+
+wg_allowedip_incremental_stealing_cleanup()
+{
+ vnet_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "wg_basic"
+ atf_add_test_case "wg_basic_crossaf"
atf_add_test_case "wg_basic_netmap"
atf_add_test_case "wg_key_peerdev_shared"
atf_add_test_case "wg_key_peerdev_makeshared"
atf_add_test_case "wg_vnet_parent_routing"
+ atf_add_test_case "wg_allowedip_incremental"
+ atf_add_test_case "wg_allowedip_incremental_inet6"
+ atf_add_test_case "wg_allowedip_incremental_stealing"
}
diff --git a/tests/sys/net/routing/Makefile b/tests/sys/net/routing/Makefile
index c98e4e2a2eaf..c725d23f15d1 100644
--- a/tests/sys/net/routing/Makefile
+++ b/tests/sys/net/routing/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
WARNS?= 1
diff --git a/tests/sys/netgraph/Makefile b/tests/sys/netgraph/Makefile
index 2240570ff46a..f0eb4928ec7f 100644
--- a/tests/sys/netgraph/Makefile
+++ b/tests/sys/netgraph/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netgraph
diff --git a/tests/sys/netgraph/ksocket.c b/tests/sys/netgraph/ksocket.c
index e97b9b3f0691..a60c17bd337f 100644
--- a/tests/sys/netgraph/ksocket.c
+++ b/tests/sys/netgraph/ksocket.c
@@ -50,26 +50,28 @@ hellocheck(int wr, int rd)
ATF_TC_WITHOUT_HEAD(udp_connect);
ATF_TC_BODY(udp_connect, tc)
{
- struct sockaddr sa = {
- .sa_family = AF_INET,
- };
- socklen_t slen = sizeof(sa);
- int ds, cs, us;
-
- ATF_REQUIRE((us = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
- ATF_REQUIRE(bind(us, &sa, sizeof(sa)) == 0);
- ATF_REQUIRE(getsockname(us, &sa, &slen) == 0);
-
struct ngm_mkpeer mkp = {
.type = NG_KSOCKET_NODE_TYPE,
.ourhook = OURHOOK,
.peerhook = "inet/dgram/udp",
};
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
+ .sin_len = sizeof(sin),
+ };
+ socklen_t slen = sizeof(sin);
+ int cs, ds, us;
+
+ ATF_REQUIRE((us = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(bind(us, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(getsockname(us, (struct sockaddr *)&sin, &slen) == 0);
+
ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0);
ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp,
sizeof(mkp)) >= 0);
ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE,
- NGM_KSOCKET_CONNECT, &sa, sizeof(sa)) >= 0);
+ NGM_KSOCKET_CONNECT, &sin, sizeof(sin)) >= 0);
hellocheck(ds, us);
}
@@ -77,18 +79,19 @@ ATF_TC_BODY(udp_connect, tc)
ATF_TC_WITHOUT_HEAD(udp_bind);
ATF_TC_BODY(udp_bind, tc)
{
+ struct ngm_mkpeer mkp = {
+ .type = NG_KSOCKET_NODE_TYPE,
+ .ourhook = OURHOOK,
+ .peerhook = "inet/dgram/udp",
+ };
struct sockaddr_in sin = {
.sin_family = AF_INET,
+ .sin_addr.s_addr = htonl(INADDR_LOOPBACK),
.sin_len = sizeof(sin),
};
struct ng_mesg *rep;
- int ds, cs, us;
+ int cs, ds, us;
- struct ngm_mkpeer mkp = {
- .type = NG_KSOCKET_NODE_TYPE,
- .ourhook = OURHOOK,
- .peerhook = "inet/dgram/udp",
- };
ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0);
ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp,
sizeof(mkp)) >= 0);
@@ -106,10 +109,72 @@ ATF_TC_BODY(udp_bind, tc)
hellocheck(us, ds);
}
+ATF_TC_WITHOUT_HEAD(udp6_connect);
+ATF_TC_BODY(udp6_connect, tc)
+{
+ struct ngm_mkpeer mkp = {
+ .type = NG_KSOCKET_NODE_TYPE,
+ .ourhook = OURHOOK,
+ .peerhook = "inet6/dgram/udp6",
+ };
+ struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ };
+ socklen_t slen = sizeof(sin6);
+ int cs, ds, us;
+
+ ATF_REQUIRE((us = socket(PF_INET6, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(bind(us, (struct sockaddr *)&sin6, sizeof(sin6)) == 0);
+ ATF_REQUIRE(getsockname(us, (struct sockaddr *)&sin6, &slen) == 0);
+
+ ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0);
+ ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp,
+ sizeof(mkp)) >= 0);
+ ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE,
+ NGM_KSOCKET_CONNECT, &sin6, sizeof(sin6)) >= 0);
+
+ hellocheck(ds, us);
+}
+
+
+ATF_TC_WITHOUT_HEAD(udp6_bind);
+ATF_TC_BODY(udp6_bind, tc)
+{
+ struct ngm_mkpeer mkp = {
+ .type = NG_KSOCKET_NODE_TYPE,
+ .ourhook = OURHOOK,
+ .peerhook = "inet6/dgram/udp6",
+ };
+ struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ .sin6_len = sizeof(sin6),
+ };
+ struct ng_mesg *rep;
+ int cs, ds, us;
+
+ ATF_REQUIRE(NgMkSockNode(NULL, &cs, &ds) == 0);
+ ATF_REQUIRE(NgSendMsg(cs, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp,
+ sizeof(mkp)) >= 0);
+ ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE,
+ NGM_KSOCKET_BIND, &sin6, sizeof(sin6)) >= 0);
+ ATF_REQUIRE(NgSendMsg(cs, ".:" OURHOOK, NGM_KSOCKET_COOKIE,
+ NGM_KSOCKET_GETNAME, NULL, 0) >= 0);
+ ATF_REQUIRE(NgAllocRecvMsg(cs, &rep, NULL) == sizeof(struct ng_mesg) +
+ sizeof(struct sockaddr_in6));
+
+ ATF_REQUIRE((us = socket(PF_INET6, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(connect(us, (struct sockaddr *)rep->data,
+ sizeof(struct sockaddr_in6)) == 0);
+
+ hellocheck(us, ds);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, udp_connect);
ATF_TP_ADD_TC(tp, udp_bind);
+ ATF_TP_ADD_TC(tp, udp6_connect);
+ ATF_TP_ADD_TC(tp, udp6_bind);
return (atf_no_error());
}
diff --git a/tests/sys/netinet/Makefile b/tests/sys/netinet/Makefile
index fb3281028aff..cc525bf24480 100644
--- a/tests/sys/netinet/Makefile
+++ b/tests/sys/netinet/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netinet
@@ -6,13 +5,16 @@ BINDIR= ${TESTSDIR}
TESTS_SUBDIRS+= libalias
-ATF_TESTS_C= ip_reass_test \
+ATF_TESTS_C= broadcast \
+ fibs_multibind_test \
+ ip_reass_test \
ip6_v4mapped_test \
so_reuseport_lb_test \
socket_afinet \
tcp_connect_port_test \
tcp_implied_connect \
tcp_md5_getsockopt \
+ udp_bindings \
udp_io
ATF_TESTS_SH= arp \
@@ -22,19 +24,31 @@ ATF_TESTS_SH= arp \
fibs_test \
forward \
lpm \
+ multicast \
output \
redirect
ATF_TESTS_PYTEST+= carp.py
ATF_TESTS_PYTEST+= igmp.py
-TEST_METADATA.divert+= required_programs="python"
-TEST_METADATA.forward+= required_programs="python"
+LIBADD.so_reuseport_lb_test= pthread
+LIBADD.udp_bindings= pthread
+
+# Some of the arp tests look for log messages in the dmesg buffer, so run them
+# serially to avoid problems with interleaved output.
+TEST_METADATA.arp+= is_exclusive="true"
+TEST_METADATA.divert+= required_programs="python" \
+ execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.fibs_test+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.forward+= required_programs="python" \
+ execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
TEST_METADATA.output+= required_programs="python"
TEST_METADATA.redirect+= required_programs="python"
-TEST_METADATA.tcp6_v4mapped_bind_test+= is_exclusive="true"
-PROGS= udp_dontroute tcp_user_cookie
+PROGS= udp_dontroute tcp_user_cookie sendto-IP_MULTICAST_IF
${PACKAGE}FILES+= redirect.py
diff --git a/tests/sys/netinet/arp.sh b/tests/sys/netinet/arp.sh
index 34946d42f250..df5dbc50ffa1 100755
--- a/tests/sys/netinet/arp.sh
+++ b/tests/sys/netinet/arp.sh
@@ -188,7 +188,9 @@ static_body() {
ipa=198.51.100.1
ipb=198.51.100.2
+ ipb_re=$(echo ${ipb} | sed 's/\./\\./g')
max_age=$(sysctl -n net.link.ether.inet.max_age)
+ max_age="(${max_age}|$((${max_age} - 1)))"
atf_check ifconfig -j ${jname}a ${epair0}a inet ${ipa}/24
eth="$(ifconfig -j ${jname}b ${epair0}b |
@@ -197,8 +199,8 @@ static_body() {
# Expected outputs
permanent=\
"? (${ipb}) at 00:00:00:00:00:00 on ${epair0}a permanent [ethernet]\n"
- temporary=\
-"? (${ipb}) at ${eth} on ${epair0}a expires in ${max_age} seconds [ethernet]\n"
+ temporary_re=\
+"\? \(${ipb_re}\) at ${eth} on ${epair0}a expires in ${max_age} seconds \[ethernet\]"
deleted=\
"${ipb} (${ipb}) deleted\n"
@@ -217,7 +219,7 @@ static_body() {
# then check -S
atf_check -o "inline:${deleted}" jexec ${jname}a arp -nd ${ipb}
atf_check -o ignore jexec ${jname}b ping -c1 ${ipa}
- atf_check -o "inline:${temporary}" jexec ${jname}a arp -n ${ipb}
+ atf_check -o "match:${temporary_re}" jexec ${jname}a arp -n ${ipb}
# Note: this doesn't fail, tracked all the way down to FreeBSD 8
# atf_check -s not-exit:0 jexec ${jname}a arp -s ${ipb} 0:0:0:0:0:0
atf_check -o "inline:${deleted}" \
@@ -229,6 +231,32 @@ static_cleanup() {
vnet_cleanup
}
+atf_test_case "garp" "cleanup"
+garp_head() {
+ atf_set descr 'Basic gratuitous arp test'
+ atf_set require.user root
+}
+
+garp_body() {
+ vnet_init
+
+ j="v4t-garp"
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail ${j} ${epair}a
+ atf_check -s exit:0 -o ignore \
+ jexec ${j} sysctl net.link.ether.inet.garp_rexmit_count=3
+ jexec ${j} ifconfig ${epair}a inet 192.0.2.1/24 up
+
+ # Allow some time for the timer to actually fire
+ sleep 5
+}
+
+garp_cleanup() {
+ vnet_cleanup
+}
+
atf_init_test_cases()
{
@@ -238,6 +266,7 @@ atf_init_test_cases()
atf_add_test_case "pending_delete_if"
atf_add_test_case "arp_lookup_host"
atf_add_test_case "static"
+ atf_add_test_case "garp"
}
# end
diff --git a/tests/sys/netinet/broadcast.c b/tests/sys/netinet/broadcast.c
new file mode 100644
index 000000000000..e7850d513663
--- /dev/null
+++ b/tests/sys/netinet/broadcast.c
@@ -0,0 +1,196 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <ifaddrs.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static const char buf[] = "Hello";
+
+/* Create a UDP socket with SO_BROADCAST set. */
+static int
+bcastsock(void)
+{
+ int s;
+
+ ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(setsockopt(s, SOL_SOCKET, SO_BROADCAST, &(int){1},
+ sizeof(int)) == 0);
+ return (s);
+}
+
+/* Send on socket 's' with address 'to', confirm receive on 'r'. */
+static void
+bcastecho(int s, struct sockaddr_in *to, int r)
+{
+ char rbuf[sizeof(buf)];
+
+ printf("Sending to %s\n", inet_ntoa(to->sin_addr));
+ ATF_REQUIRE_MSG(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)to,
+ sizeof(*to)) == sizeof(buf), "sending of broadcast failed: %d",
+ errno);
+ ATF_REQUIRE(recv(r, rbuf, sizeof(rbuf), 0) == sizeof(rbuf));
+ ATF_REQUIRE_MSG(memcmp(buf, rbuf, sizeof(buf)) == 0,
+ "failed to receive own broadcast");
+}
+
+/* Find a first broadcast capable interface and copy its broadcast address. */
+static void
+firstbcast(struct in_addr *out)
+{
+ struct ifaddrs *ifa0, *ifa;
+ struct sockaddr_in sin;
+
+ ATF_REQUIRE(getifaddrs(&ifa0) == 0);
+ for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next)
+ if (ifa->ifa_addr->sa_family == AF_INET &&
+ (ifa->ifa_flags & IFF_BROADCAST))
+ break;
+ if (ifa == NULL) {
+ freeifaddrs(ifa0);
+ atf_tc_skip("No broadcast address found");
+ }
+ memcpy(&sin, ifa->ifa_broadaddr, sizeof(struct sockaddr_in));
+ *out = sin.sin_addr;
+ freeifaddrs(ifa0);
+}
+
+/* Application sends to INADDR_BROADCAST, and this goes on the wire. */
+ATF_TC(INADDR_BROADCAST);
+ATF_TC_HEAD(INADDR_BROADCAST, tc)
+{
+ atf_tc_set_md_var(tc, "require.config", "allow_network_access");
+}
+ATF_TC_BODY(INADDR_BROADCAST, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ socklen_t slen = sizeof(sin);
+ int l, s;
+
+ l = bcastsock();
+ ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0);
+ sin.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+
+ s = bcastsock();
+ bcastecho(s, &sin, l);
+
+ close(s);
+ close(l);
+}
+
+/*
+ * Application sends on broadcast address of an interface, INADDR_BROADCAST
+ * goes on the wire of the selected interface.
+ */
+ATF_TC_WITHOUT_HEAD(IP_ONESBCAST);
+ATF_TC_BODY(IP_ONESBCAST, tc)
+{
+ struct ifaddrs *ifa0, *ifa;
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ socklen_t slen = sizeof(sin);
+ int s, l;
+ in_port_t port;
+ bool skip = true;
+
+ s = bcastsock();
+ ATF_REQUIRE(setsockopt(s, IPPROTO_IP, IP_ONESBCAST, &(int){1},
+ sizeof(int)) == 0);
+
+ l = bcastsock();
+ ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0);
+ port = sin.sin_port;
+
+ ATF_REQUIRE(getifaddrs(&ifa0) == 0);
+ for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) {
+ if (ifa->ifa_addr->sa_family != AF_INET)
+ continue;
+ if (!(ifa->ifa_flags & IFF_BROADCAST))
+ continue;
+ skip = false;
+ memcpy(&sin, ifa->ifa_broadaddr, sizeof(struct sockaddr_in));
+ sin.sin_port = port;
+ bcastecho(s, &sin, l);
+ }
+ freeifaddrs(ifa0);
+ close(s);
+ close(l);
+ if (skip)
+ atf_tc_skip("No broadcast address found");
+}
+
+/*
+ * Application sends on broadcast address of an interface, and this is what
+ * goes out the wire.
+ */
+ATF_TC_WITHOUT_HEAD(local_broadcast);
+ATF_TC_BODY(local_broadcast, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ socklen_t slen = sizeof(sin);
+ int s, l;
+
+ s = bcastsock();
+ l = bcastsock();
+ ATF_REQUIRE(bind(l, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(getsockname(l, (struct sockaddr *)&sin, &slen) == 0);
+ firstbcast(&sin.sin_addr);
+
+ bcastecho(s, &sin, l);
+
+ close(s);
+ close(l);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, INADDR_BROADCAST);
+ ATF_TP_ADD_TC(tp, IP_ONESBCAST);
+ ATF_TP_ADD_TC(tp, local_broadcast);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/carp.py b/tests/sys/netinet/carp.py
index ac2c5547fe97..e35c9470d035 100644
--- a/tests/sys/netinet/carp.py
+++ b/tests/sys/netinet/carp.py
@@ -39,6 +39,7 @@ class TestCarp(VnetTestTemplate):
if p.src != "00:00:5e:00:01:01":
raise
+ @pytest.mark.require_progs(["scapy"])
def test_source_mac(self):
"Test carp packets source address"
@@ -51,3 +52,18 @@ class TestCarp(VnetTestTemplate):
carp_pkts = sc.sniff(iface=if1.name, stop_filter=filter_f, timeout=5)
self.check_carp_src_mac(carp_pkts)
+
+ @pytest.mark.require_progs(["scapy"])
+ def test_source_mac_vrrp(self):
+ "Test VRRP packets source address"
+
+ if1 = self.vnet.iface_alias_map["if1"]
+
+ ToolsHelper.print_output(
+ "ifconfig {} add vhid 1 carpver 3 192.0.2.203/24".format(if1.name)
+ )
+
+ carp_pkts = sc.sniff(iface=if1.name, stop_filter=filter_f, timeout=5)
+
+ self.check_carp_src_mac(carp_pkts)
+
diff --git a/tests/sys/netinet/carp.sh b/tests/sys/netinet/carp.sh
index d08940d433d7..2aae2854826e 100755
--- a/tests/sys/netinet/carp.sh
+++ b/tests/sys/netinet/carp.sh
@@ -31,7 +31,7 @@ is_master()
jail=$1
itf=$2
- jexec ${jail} ifconfig ${itf} | grep carp | grep MASTER
+ jexec ${jail} ifconfig ${itf} | grep -E '(carp|vrrp)' | grep MASTER
}
wait_for_carp()
@@ -71,6 +71,7 @@ basic_v4_head()
basic_v4_body()
{
carp_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair_one=$(vnet_mkepair)
@@ -105,6 +106,52 @@ basic_v4_cleanup()
vnet_cleanup
}
+atf_test_case "vrrp_v4" "cleanup"
+vrrp_v4_head()
+{
+ atf_set descr 'Basic VRRP test (IPv4)'
+ atf_set require.user root
+}
+
+vrrp_v4_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ j=vrrp_basic_v4
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail ${j}_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail ${j}_two ${epair_one}b
+ vnet_mkjail ${j}_three ${epair_two}b
+
+ jexec ${j}_one ifconfig ${bridge} 192.0.2.4/29 up
+ jexec ${j}_one ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec ${j}_one ifconfig ${epair_one}a up
+ jexec ${j}_one ifconfig ${epair_two}a up
+
+ jexec ${j}_two ifconfig ${epair_one}b 192.0.2.202/29 up
+ jexec ${j}_two ifconfig ${epair_one}b add vhid 1 carpver 3 192.0.2.1/29
+
+ jexec ${j}_three ifconfig ${epair_two}b 192.0.2.203/29 up
+ jexec ${j}_three ifconfig ${epair_two}b add vhid 1 carpver 3 \
+ 192.0.2.1/29
+
+ wait_for_carp ${j}_two ${epair_one}b \
+ ${j}_three ${epair_two}b
+
+ atf_check -s exit:0 -o ignore jexec ${j}_one \
+ ping -c 3 192.0.2.1
+}
+
+vrrp_v4_cleanup()
+{
+ vnet_cleanup
+}
atf_test_case "unicast_v4" "cleanup"
unicast_v4_head()
@@ -117,45 +164,53 @@ unicast_v4_body()
{
carp_init
- bridge=$(vnet_mkbridge)
epair_one=$(vnet_mkepair)
epair_two=$(vnet_mkepair)
- vnet_mkjail carp_uni_v4_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail carp_uni_v4_one ${epair_one}a ${epair_two}a
vnet_mkjail carp_uni_v4_two ${epair_one}b
vnet_mkjail carp_uni_v4_three ${epair_two}b
- jexec carp_uni_v4_one ifconfig ${bridge} 192.0.2.4/29 up
jexec carp_uni_v4_one sysctl net.inet.ip.forwarding=1
- jexec carp_uni_v4_one ifconfig ${bridge} addm ${epair_one}a \
- addm ${epair_two}a
- jexec carp_uni_v4_one ifconfig ${epair_one}a up
- jexec carp_uni_v4_one ifconfig ${epair_two}a up
- jexec carp_uni_v4_one ifconfig ${bridge} inet alias 198.51.100.1/25
- jexec carp_uni_v4_one ifconfig ${bridge} inet alias 198.51.100.129/25
+ jexec carp_uni_v4_one ifconfig ${epair_one}a inet 198.51.100.1/25
+ jexec carp_uni_v4_one ifconfig ${epair_two}a inet 198.51.100.129/25
+ jexec carp_uni_v4_two sysctl net.inet.ip.forwarding=1
jexec carp_uni_v4_two ifconfig ${epair_one}b 198.51.100.2/25 up
- jexec carp_uni_v4_two route add default 198.51.100.1
+ jexec carp_uni_v4_two route add 198.51.100.224 198.51.100.1
+ # A peer address x.x.x.224 to catch PR 284872
jexec carp_uni_v4_two ifconfig ${epair_one}b add vhid 1 \
- peer 198.51.100.130 192.0.2.1/29
+ peer 198.51.100.224 192.0.2.1/32
- jexec carp_uni_v4_three ifconfig ${epair_two}b 198.51.100.130/25 up
- jexec carp_uni_v4_three route add default 198.51.100.129
+ jexec carp_uni_v4_three sysctl net.inet.ip.forwarding=1
+ jexec carp_uni_v4_three ifconfig ${epair_two}b 198.51.100.224/25 up
+ jexec carp_uni_v4_three route add 198.51.100.2 198.51.100.129
jexec carp_uni_v4_three ifconfig ${epair_two}b add vhid 1 \
- peer 198.51.100.2 192.0.2.1/29
+ peer 198.51.100.2 192.0.2.1/32
# Sanity check
atf_check -s exit:0 -o ignore jexec carp_uni_v4_two \
- ping -c 1 198.51.100.130
+ ping -c 1 198.51.100.224
wait_for_carp carp_uni_v4_two ${epair_one}b \
carp_uni_v4_three ${epair_two}b
+ # Setup RIPv2 route daemon
+ jexec carp_uni_v4_two routed -s -Pripv2
+ jexec carp_uni_v4_three routed -s -Pripv2
+ jexec carp_uni_v4_one routed -Pripv2
+
+ # XXX Wait for route propagation
+ sleep 3
+
atf_check -s exit:0 -o ignore jexec carp_uni_v4_one \
ping -c 3 192.0.2.1
- jexec carp_uni_v4_two ifconfig
- jexec carp_uni_v4_three ifconfig
+ # Check that we remain in unicast when tweaking settings
+ atf_check -s exit:0 -o ignore \
+ jexec carp_uni_v4_two ifconfig ${epair_one}b vhid 1 advskew 2
+ atf_check -s exit:0 -o match:"peer 198.51.100.224" \
+ jexec carp_uni_v4_two ifconfig ${epair_one}b
}
unicast_v4_cleanup()
@@ -173,6 +228,7 @@ basic_v6_head()
basic_v6_body()
{
carp_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair_one=$(vnet_mkepair)
@@ -210,6 +266,56 @@ basic_v6_cleanup()
vnet_cleanup
}
+atf_test_case "vrrp_v6" "cleanup"
+vrrp_v6_head()
+{
+ atf_set descr 'Basic VRRP test (IPv6)'
+ atf_set require.user root
+}
+
+vrrp_v6_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ j=carp_basic_v6
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail ${j}_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail ${j}_two ${epair_one}b
+ vnet_mkjail ${j}_three ${epair_two}b
+
+ jexec ${j}_one ifconfig ${bridge} inet6 2001:db8::0:4/64 up \
+ no_dad
+ jexec ${j}_one ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec ${j}_one ifconfig ${epair_one}a up
+ jexec ${j}_one ifconfig ${epair_two}a up
+
+ jexec ${j}_two ifconfig ${epair_one}b inet6 \
+ 2001:db8::1:2/64 up no_dad
+ jexec ${j}_two ifconfig ${epair_one}b inet6 add vhid 1 carpver 3 \
+ 2001:db8::0:1/64
+
+ jexec ${j}_three ifconfig ${epair_two}b inet6 2001:db8::1:3/64 up no_dad
+ jexec ${j}_three ifconfig ${epair_two}b inet6 add vhid 1 carpver 3 \
+ 2001:db8::0:1/64
+
+ wait_for_carp ${j}_two ${epair_one}b \
+ ${j}_three ${epair_two}b
+
+ atf_check -s exit:0 -o ignore jexec ${j}_one \
+ ping -6 -c 3 2001:db8::0:1
+}
+
+vrrp_v6_cleanup()
+{
+ vnet_cleanup
+}
+
atf_test_case "unicast_v6" "cleanup"
unicast_v6_head()
{
@@ -220,6 +326,7 @@ unicast_v6_head()
unicast_v6_body()
{
carp_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair_one=$(vnet_mkepair)
@@ -281,6 +388,7 @@ unicast_ll_v6_head()
unicast_ll_v6_body()
{
carp_init
+ vnet_init_bridge
j=carp_uni_ll_v6
@@ -391,6 +499,7 @@ nd6_ns_source_mac_head()
nd6_ns_source_mac_body()
{
carp_init
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
epair_one=$(vnet_mkepair)
@@ -469,8 +578,10 @@ switch_cleanup()
atf_init_test_cases()
{
atf_add_test_case "basic_v4"
+ atf_add_test_case "vrrp_v4"
atf_add_test_case "unicast_v4"
atf_add_test_case "basic_v6"
+ atf_add_test_case "vrrp_v6"
atf_add_test_case "unicast_v6"
atf_add_test_case "unicast_ll_v6"
atf_add_test_case "negative_demotion"
diff --git a/tests/sys/netinet/divert.sh b/tests/sys/netinet/divert.sh
index c3b1896b995e..d50620d94a09 100755
--- a/tests/sys/netinet/divert.sh
+++ b/tests/sys/netinet/divert.sh
@@ -41,7 +41,7 @@ ipdivert_ip_output_remote_success_head() {
atf_set descr 'Test diverting IPv4 packet to remote destination'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
ipdivert_ip_output_remote_success_body() {
@@ -96,7 +96,7 @@ ipdivert_ip_input_local_success_head() {
atf_set descr 'Test diverting IPv4 packet to remote destination'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
ipdivert_ip_input_local_success_body() {
diff --git a/tests/sys/netinet/fibs_multibind_test.c b/tests/sys/netinet/fibs_multibind_test.c
new file mode 100644
index 000000000000..61ebf83c56ef
--- /dev/null
+++ b/tests/sys/netinet/fibs_multibind_test.c
@@ -0,0 +1,755 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024-2025 Stormshield
+ */
+
+#include <sys/types.h>
+#include <sys/ioctl.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip6.h>
+#include <netinet/ip_icmp.h>
+#include <netinet/icmp6.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <atf-c.h>
+
+#define MAKETEST_TCP(name) \
+ATF_TC_WITHOUT_HEAD(name ## _tcp); \
+ATF_TC_BODY(name ## _tcp, tc) \
+{ \
+ name(PF_INET, SOCK_STREAM, tc); \
+} \
+ATF_TC_WITHOUT_HEAD(name ## _tcp6); \
+ATF_TC_BODY(name ## _tcp6, tc) \
+{ \
+ name(PF_INET6, SOCK_STREAM, tc); \
+}
+#define MAKETEST_UDP(name) \
+ATF_TC_WITHOUT_HEAD(name ## _udp); \
+ATF_TC_BODY(name ## _udp, tc) \
+{ \
+ name(PF_INET, SOCK_DGRAM, tc); \
+} \
+ATF_TC_WITHOUT_HEAD(name ## _udp6); \
+ATF_TC_BODY(name ## _udp6, tc) \
+{ \
+ name(PF_INET6, SOCK_DGRAM, tc); \
+}
+#define MAKETEST_RAW(name) \
+ATF_TC(name ## _raw); \
+ATF_TC_HEAD(name ## _raw, tc) \
+{ \
+ atf_tc_set_md_var(tc, "require.user", \
+ "root"); \
+} \
+ATF_TC_BODY(name ## _raw, tc) \
+{ \
+ name(PF_INET, SOCK_RAW, tc); \
+} \
+ATF_TC(name ## _raw6); \
+ATF_TC_HEAD(name ## _raw6, tc) \
+{ \
+ atf_tc_set_md_var(tc, "require.user", \
+ "root"); \
+} \
+ATF_TC_BODY(name ## _raw6, tc) \
+{ \
+ name(PF_INET6, SOCK_RAW, tc); \
+}
+
+#define MAKETEST(name) \
+ MAKETEST_TCP(name) \
+ MAKETEST_UDP(name)
+
+#define LISTTEST_TCP(name) \
+ ATF_TP_ADD_TC(tp, name ## _tcp); \
+ ATF_TP_ADD_TC(tp, name ## _tcp6);
+#define LISTTEST_UDP(name) \
+ ATF_TP_ADD_TC(tp, name ## _udp); \
+ ATF_TP_ADD_TC(tp, name ## _udp6);
+#define LISTTEST_RAW(name) \
+ ATF_TP_ADD_TC(tp, name ## _raw); \
+ ATF_TP_ADD_TC(tp, name ## _raw6);
+#define LISTTEST(name) \
+ LISTTEST_TCP(name) \
+ LISTTEST_UDP(name)
+
+static void
+checked_close(int s)
+{
+ int error;
+
+ error = close(s);
+ ATF_REQUIRE_MSG(error == 0, "close failed: %s", strerror(errno));
+}
+
+static int
+mksockp(int domain, int type, int fib, int proto)
+{
+ int error, s;
+
+ s = socket(domain, type, proto);
+ ATF_REQUIRE(s != -1);
+ error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &fib, sizeof(fib));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ return (s);
+}
+
+static int
+mksock(int domain, int type, int fib)
+{
+ return (mksockp(domain, type, fib, 0));
+}
+
+static void
+require_fibs_multibind(int socktype, int minfibs)
+{
+ const char *sysctl;
+ size_t sz;
+ int error, fibs, multibind;
+
+ fibs = 0;
+ sz = sizeof(fibs);
+ error = sysctlbyname("net.fibs", &fibs, &sz, NULL, 0);
+ ATF_REQUIRE_MSG(error == 0, "sysctlbyname failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(fibs >= 1, "strange FIB count %d", fibs);
+ if (fibs == 1)
+ atf_tc_skip("multiple FIBs not enabled");
+ if (fibs < minfibs)
+ atf_tc_skip("not enough FIBs, need %d", minfibs);
+
+ switch (socktype) {
+ case SOCK_STREAM:
+ sysctl = "net.inet.tcp.bind_all_fibs";
+ break;
+ case SOCK_DGRAM:
+ sysctl = "net.inet.udp.bind_all_fibs";
+ break;
+ case SOCK_RAW:
+ sysctl = "net.inet.raw.bind_all_fibs";
+ break;
+ default:
+ atf_tc_fail("unknown socket type %d", socktype);
+ break;
+ }
+
+ multibind = -1;
+ sz = sizeof(multibind);
+ error = sysctlbyname(sysctl, &multibind, &sz, NULL, 0);
+ ATF_REQUIRE_MSG(error == 0, "sysctlbyname failed: %s", strerror(errno));
+ if (multibind != 0)
+ atf_tc_skip("FIB multibind not configured (%s)", sysctl);
+}
+
+/*
+ * Make sure that different users can't bind to the same port from different
+ * FIBs.
+ */
+static void
+multibind_different_user(int domain, int type, const atf_tc_t *tc)
+{
+ struct sockaddr_storage ss;
+ struct passwd *passwd;
+ const char *user;
+ socklen_t sslen;
+ int error, s[2];
+
+ if (geteuid() != 0)
+ atf_tc_skip("need root privileges");
+ if (!atf_tc_has_config_var(tc, "unprivileged_user"))
+ atf_tc_skip("unprivileged_user not set");
+
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+
+ require_fibs_multibind(type, 2);
+
+ s[0] = mksock(domain, type, 0);
+
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(s[0], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ error = getsockname(s[0], (struct sockaddr *)&ss, &sslen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ /*
+ * Create a second socket in a different FIB, and bind it to the same
+ * address/port tuple. This should succeed if done as the same user as
+ * the first socket, and should fail otherwise.
+ */
+ s[1] = mksock(domain, type, 1);
+ error = bind(s[1], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(close(s[1]) == 0, "close failed: %s", strerror(errno));
+
+ user = atf_tc_get_config_var(tc, "unprivileged_user");
+ passwd = getpwnam(user);
+ ATF_REQUIRE(passwd != NULL);
+ error = seteuid(passwd->pw_uid);
+ ATF_REQUIRE_MSG(error == 0, "seteuid failed: %s", strerror(errno));
+
+ /* Repeat the bind as a different user. */
+ s[1] = mksock(domain, type, 1);
+ error = bind(s[1], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_ERRNO(EADDRINUSE, error == -1);
+ ATF_REQUIRE_MSG(close(s[1]) == 0, "close failed: %s", strerror(errno));
+}
+MAKETEST(multibind_different_user);
+
+/*
+ * Verify that a listening socket only accepts connections originating from the
+ * same FIB.
+ */
+static void
+per_fib_listening_socket(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_storage ss;
+ socklen_t sslen;
+ int cs1, cs2, error, fib1, fib2, ls1, ls2, ns;
+
+ ATF_REQUIRE(type == SOCK_STREAM);
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ fib1 = 0;
+ fib2 = 1;
+
+ ls1 = mksock(domain, type, fib1);
+ ls2 = mksock(domain, type, fib2);
+
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(ls1, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ error = getsockname(ls1, (struct sockaddr *)&ss, &sslen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ error = listen(ls1, 5);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ cs1 = mksock(domain, type, fib1);
+ cs2 = mksock(domain, type, fib2);
+
+ /*
+ * Make sure we can connect from the same FIB.
+ */
+ error = connect(cs1, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno));
+ ns = accept(ls1, NULL, NULL);
+ ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno));
+ checked_close(ns);
+ checked_close(cs1);
+ cs1 = mksock(domain, type, fib1);
+
+ /*
+ * ... but not from a different FIB.
+ */
+ error = connect(cs2, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == -1, "connect succeeded unexpectedly");
+ ATF_REQUIRE_MSG(errno == ECONNREFUSED, "unexpected error %d", errno);
+ checked_close(cs2);
+ cs2 = mksock(domain, type, fib2);
+
+ /*
+ * ... but if there are multiple listening sockets, we always connect to
+ * the same FIB.
+ */
+ error = bind(ls2, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(ls2, 5);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ for (int i = 0; i < 10; i++) {
+ error = connect(cs1, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s",
+ strerror(errno));
+ ns = accept(ls1, NULL, NULL);
+ ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno));
+
+ checked_close(ns);
+ checked_close(cs1);
+ cs1 = mksock(domain, type, fib1);
+ }
+ for (int i = 0; i < 10; i++) {
+ error = connect(cs2, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s",
+ strerror(errno));
+ ns = accept(ls2, NULL, NULL);
+ ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno));
+
+ checked_close(ns);
+ checked_close(cs2);
+ cs2 = mksock(domain, type, fib2);
+ }
+
+ /*
+ * ... and if we close one of the listening sockets, we're back to only
+ * being able to connect from the same FIB.
+ */
+ checked_close(ls1);
+ error = connect(cs1, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == -1, "connect succeeded unexpectedly");
+ ATF_REQUIRE_MSG(errno == ECONNREFUSED, "unexpected error %d", errno);
+ checked_close(cs1);
+
+ error = connect(cs2, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno));
+ ns = accept(ls2, NULL, NULL);
+ ATF_REQUIRE_MSG(ns != -1, "accept failed: %s", strerror(errno));
+ checked_close(ns);
+ checked_close(cs2);
+ checked_close(ls2);
+}
+MAKETEST_TCP(per_fib_listening_socket);
+
+/*
+ * Verify that a bound datagram socket only accepts data from the same FIB.
+ */
+static void
+per_fib_dgram_socket(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_storage ss;
+ struct sockaddr_in6 *sin6p;
+ socklen_t sslen;
+ ssize_t n;
+ int error, cs1, cs2, fib1, fib2, ss1, ss2;
+ char b;
+
+ ATF_REQUIRE(type == SOCK_DGRAM);
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ fib1 = 0;
+ fib2 = 1;
+
+ cs1 = mksock(domain, type, fib1);
+ cs2 = mksock(domain, type, fib2);
+
+ ss1 = mksock(domain, type, fib1);
+ ss2 = mksock(domain, type, fib2);
+
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(ss1, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ error = getsockname(ss1, (struct sockaddr *)&ss, &sslen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ if (domain == PF_INET6) {
+ sin6p = (struct sockaddr_in6 *)&ss;
+ sin6p->sin6_addr = in6addr_loopback;
+ }
+
+ /* If we send a byte from cs1, it should be recieved by ss1. */
+ b = 42;
+ n = sendto(cs1, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ n = recv(ss1, &b, sizeof(b), 0);
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 42);
+
+ /* If we send a byte from cs2, it should not be received by ss1. */
+ b = 42;
+ n = sendto(cs2, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ usleep(10000);
+ n = recv(ss1, &b, sizeof(b), MSG_DONTWAIT);
+ ATF_REQUIRE_ERRNO(EWOULDBLOCK, n == -1);
+
+ error = bind(ss2, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ /* Repeat now that ss2 is bound. */
+ b = 42;
+ n = sendto(cs1, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ n = recv(ss1, &b, sizeof(b), 0);
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 42);
+
+ b = 42;
+ n = sendto(cs2, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ n = recv(ss2, &b, sizeof(b), 0);
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 42);
+
+ checked_close(ss1);
+ checked_close(ss2);
+ checked_close(cs1);
+ checked_close(cs2);
+}
+MAKETEST_UDP(per_fib_dgram_socket);
+
+static size_t
+ping(int s, const struct sockaddr *sa, socklen_t salen)
+{
+ struct {
+ struct icmphdr icmp;
+ char data[64];
+ } icmp;
+ ssize_t n;
+
+ memset(&icmp, 0, sizeof(icmp));
+ icmp.icmp.icmp_type = ICMP_ECHO;
+ icmp.icmp.icmp_code = 0;
+ icmp.icmp.icmp_cksum = htons((unsigned short)~(ICMP_ECHO << 8));
+ n = sendto(s, &icmp, sizeof(icmp), 0, sa, salen);
+ ATF_REQUIRE_MSG(n == (ssize_t)sizeof(icmp), "sendto failed: %s",
+ strerror(errno));
+
+ return (sizeof(icmp) + sizeof(struct ip));
+}
+
+static size_t
+ping6(int s, const struct sockaddr *sa, socklen_t salen)
+{
+ struct {
+ struct icmp6_hdr icmp6;
+ char data[64];
+ } icmp6;
+ ssize_t n;
+
+ memset(&icmp6, 0, sizeof(icmp6));
+ icmp6.icmp6.icmp6_type = ICMP6_ECHO_REQUEST;
+ icmp6.icmp6.icmp6_code = 0;
+ icmp6.icmp6.icmp6_cksum =
+ htons((unsigned short)~(ICMP6_ECHO_REQUEST << 8));
+ n = sendto(s, &icmp6, sizeof(icmp6), 0, sa, salen);
+ ATF_REQUIRE_MSG(n == (ssize_t)sizeof(icmp6), "sendto failed: %s",
+ strerror(errno));
+
+ return (sizeof(icmp6));
+}
+
+static void
+per_fib_raw_socket(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ ssize_t n;
+ size_t sz;
+ int error, cs, s[2], proto;
+ uint8_t b[256];
+
+ ATF_REQUIRE(type == SOCK_RAW);
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ proto = domain == PF_INET ? IPPROTO_ICMP : IPPROTO_ICMPV6;
+ s[0] = mksockp(domain, type, 0, proto);
+ s[1] = mksockp(domain, type, 1, proto);
+
+ if (domain == PF_INET) {
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = domain;
+ sin.sin_len = sizeof(sin);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ error = bind(s[0], (struct sockaddr *)&sin, sizeof(sin));
+ } else /* if (domain == PF_INET6) */ {
+ memset(&sin6, 0, sizeof(sin6));
+ sin6.sin6_family = domain;
+ sin6.sin6_len = sizeof(sin6);
+ sin6.sin6_addr = in6addr_loopback;
+ error = bind(s[0], (struct sockaddr *)&sin6, sizeof(sin6));
+ }
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ for (int i = 0; i < 2; i++) {
+ cs = mksockp(domain, type, i, proto);
+ if (domain == PF_INET) {
+ sz = ping(cs, (struct sockaddr *)&sin, sizeof(sin));
+ } else /* if (domain == PF_INET6) */ {
+ sz = ping6(cs, (struct sockaddr *)&sin6, sizeof(sin6));
+ }
+ n = recv(s[i], b, sizeof(b), 0);
+ ATF_REQUIRE_MSG(n > 0, "recv failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(n == (ssize_t)sz,
+ "short packet received: %zd", n);
+
+ if (domain == PF_INET6) {
+ /* Get the echo reply as well. */
+ n = recv(s[i], b, sizeof(b), 0);
+ ATF_REQUIRE_MSG(n > 0,
+ "recv failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(n == (ssize_t)sz,
+ "short packet received: %zd", n);
+ }
+
+ /* Make sure that the other socket didn't receive anything. */
+ n = recv(s[1 - i], b, sizeof(b), MSG_DONTWAIT);
+ printf("n = %zd i = %d\n", n, i);
+ ATF_REQUIRE_ERRNO(EWOULDBLOCK, n == -1);
+
+ checked_close(cs);
+ }
+
+ checked_close(s[0]);
+ checked_close(s[1]);
+}
+MAKETEST_RAW(per_fib_raw_socket);
+
+/*
+ * Create a pair of load-balancing listening socket groups, one in each FIB, and
+ * make sure that connections to the group are only load-balanced within the
+ * same FIB.
+ */
+static void
+multibind_lbgroup_stream(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_storage ss;
+ socklen_t sslen;
+ int error, as, cs, s[3];
+
+ ATF_REQUIRE(type == SOCK_STREAM);
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ s[0] = mksock(domain, type, 0);
+ ATF_REQUIRE(setsockopt(s[0], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+ ATF_REQUIRE(fcntl(s[0], F_SETFL, O_NONBLOCK) == 0);
+ s[1] = mksock(domain, type, 0);
+ ATF_REQUIRE(setsockopt(s[1], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+ ATF_REQUIRE(fcntl(s[1], F_SETFL, O_NONBLOCK) == 0);
+ s[2] = mksock(domain, type, 1);
+ ATF_REQUIRE(setsockopt(s[2], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(s[0], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(s[0], 5);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+ error = getsockname(s[0], (struct sockaddr *)&ss, &sslen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ error = bind(s[1], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(s[1], 5);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ error = bind(s[2], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(s[2], 5);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ /*
+ * Initiate connections from FIB 0, make sure they go to s[0] or s[1].
+ */
+ for (int count = 0; count < 100; count++) {
+ cs = mksock(domain, type, 0);
+ error = connect(cs, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s",
+ strerror(errno));
+
+ do {
+ as = accept(s[0], NULL, NULL);
+ if (as == -1) {
+ ATF_REQUIRE_MSG(errno == EWOULDBLOCK,
+ "accept failed: %s", strerror(errno));
+ as = accept(s[1], NULL, NULL);
+ if (as == -1) {
+ ATF_REQUIRE_MSG(errno == EWOULDBLOCK,
+ "accept failed: %s",
+ strerror(errno));
+ }
+ }
+ } while (as == -1);
+ checked_close(as);
+ checked_close(cs);
+ }
+
+ /*
+ * Initiate connections from FIB 1, make sure they go to s[2].
+ */
+ for (int count = 0; count < 100; count++) {
+ cs = mksock(domain, type, 1);
+ error = connect(cs, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s",
+ strerror(errno));
+
+ as = accept(s[2], NULL, NULL);
+ ATF_REQUIRE_MSG(as != -1, "accept failed: %s", strerror(errno));
+ checked_close(as);
+ checked_close(cs);
+ }
+
+ checked_close(s[0]);
+ checked_close(s[1]);
+ checked_close(s[2]);
+}
+MAKETEST_TCP(multibind_lbgroup_stream);
+
+static void
+multibind_lbgroup_dgram(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_storage ss;
+ struct sockaddr_in6 *sin6p;
+ socklen_t sslen;
+ ssize_t n;
+ int error, cs, s[3];
+ char b;
+
+ ATF_REQUIRE(type == SOCK_DGRAM);
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ s[0] = mksock(domain, type, 0);
+ ATF_REQUIRE(setsockopt(s[0], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+ s[1] = mksock(domain, type, 0);
+ ATF_REQUIRE(setsockopt(s[1], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+ s[2] = mksock(domain, type, 1);
+ ATF_REQUIRE(setsockopt(s[2], SOL_SOCKET, SO_REUSEPORT_LB, &(int){1},
+ sizeof(int)) == 0);
+
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(s[0], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = getsockname(s[0], (struct sockaddr *)&ss, &sslen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+
+ error = bind(s[1], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = bind(s[2], (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ if (domain == PF_INET6) {
+ sin6p = (struct sockaddr_in6 *)&ss;
+ sin6p->sin6_addr = in6addr_loopback;
+ }
+
+ /*
+ * Send a packet from FIB 0, make sure it goes to s[0] or s[1].
+ */
+ cs = mksock(domain, type, 0);
+ for (int count = 0; count < 100; count++) {
+ int bytes, rs;
+
+ b = 42;
+ n = sendto(cs, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ usleep(1000);
+
+ error = ioctl(s[0], FIONREAD, &bytes);
+ ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s",
+ strerror(errno));
+ if (bytes == 0) {
+ error = ioctl(s[1], FIONREAD, &bytes);
+ ATF_REQUIRE_MSG(error == 0, "ioctl failed: %s",
+ strerror(errno));
+ rs = s[1];
+ } else {
+ rs = s[0];
+ }
+ n = recv(rs, &b, sizeof(b), 0);
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 42);
+ ATF_REQUIRE(bytes == 1);
+ }
+ checked_close(cs);
+
+ /*
+ * Send a packet from FIB 1, make sure it goes to s[2].
+ */
+ cs = mksock(domain, type, 1);
+ for (int count = 0; count < 100; count++) {
+ b = 42;
+ n = sendto(cs, &b, sizeof(b), 0, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(n == 1, "sendto failed: %s", strerror(errno));
+ usleep(1000);
+
+ n = recv(s[2], &b, sizeof(b), 0);
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 42);
+ }
+ checked_close(cs);
+
+ checked_close(s[0]);
+ checked_close(s[1]);
+ checked_close(s[2]);
+}
+MAKETEST_UDP(multibind_lbgroup_dgram);
+
+/*
+ * Make sure that we can't change the FIB of a bound socket.
+ */
+static void
+no_setfib_after_bind(int domain, int type, const atf_tc_t *tc __unused)
+{
+ struct sockaddr_storage ss;
+ socklen_t sslen;
+ int error, s;
+
+ ATF_REQUIRE(domain == PF_INET || domain == PF_INET6);
+ require_fibs_multibind(type, 2);
+
+ s = mksock(domain, type, 0);
+
+ sslen = domain == PF_INET ? sizeof(struct sockaddr_in) :
+ sizeof(struct sockaddr_in6);
+ memset(&ss, 0, sizeof(ss));
+ ss.ss_family = domain;
+ ss.ss_len = sslen;
+ error = bind(s, (struct sockaddr *)&ss, sslen);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+
+ error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &(int){1}, sizeof(int));
+ ATF_REQUIRE_ERRNO(EISCONN, error == -1);
+
+ /* It's ok to set the FIB number to its current value. */
+ error = setsockopt(s, SOL_SOCKET, SO_SETFIB, &(int){0}, sizeof(int));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt failed: %s", strerror(errno));
+
+ checked_close(s);
+}
+MAKETEST(no_setfib_after_bind);
+
+ATF_TP_ADD_TCS(tp)
+{
+ LISTTEST(multibind_different_user);
+ LISTTEST_TCP(per_fib_listening_socket);
+ LISTTEST_UDP(per_fib_dgram_socket);
+ LISTTEST_RAW(per_fib_raw_socket);
+ LISTTEST_TCP(multibind_lbgroup_stream);
+ LISTTEST_UDP(multibind_lbgroup_dgram);
+ LISTTEST(no_setfib_after_bind);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/fibs_test.sh b/tests/sys/netinet/fibs_test.sh
index 5c1a918abb2c..2d0b63f8e30a 100644
--- a/tests/sys/netinet/fibs_test.sh
+++ b/tests/sys/netinet/fibs_test.sh
@@ -30,9 +30,6 @@
# Authors: Alan Somers (Spectra Logic Corporation)
#
-# All of the tests in this file requires the test-suite config variable "fibs"
-# to be defined to a space-delimited list of FIBs that may be used for testing.
-
# arpresolve should check the interface fib for routes to a target when
# creating an ARP table entry. This is a regression for kern/167947, where
# arpresolve only checked the default route.
@@ -48,7 +45,6 @@ arpresolve_checks_interface_fib_head()
{
atf_set "descr" "arpresolve should check the interface fib, not the default fib, for routes"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
atf_set "require.progs" "nping"
}
arpresolve_checks_interface_fib_body()
@@ -100,7 +96,6 @@ loopback_and_network_routes_on_nondefault_fib_head()
{
atf_set "descr" "When creating and deleting loopback IPv4 routes, use the interface's fib"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
loopback_and_network_routes_on_nondefault_fib_body()
@@ -157,7 +152,6 @@ loopback_and_network_routes_on_nondefault_fib_inet6_head()
{
atf_set "descr" "When creating and deleting loopback IPv6 routes, use the interface's fib"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
loopback_and_network_routes_on_nondefault_fib_inet6_body()
@@ -216,7 +210,6 @@ default_route_with_multiple_fibs_on_same_subnet_head()
{
atf_set "descr" "Multiple interfaces on the same subnet but with different fibs can both have default IPv4 routes"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
default_route_with_multiple_fibs_on_same_subnet_body()
@@ -263,7 +256,6 @@ default_route_with_multiple_fibs_on_same_subnet_inet6_head()
{
atf_set "descr" "Multiple interfaces on the same subnet but with different fibs can both have default IPv6 routes"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
default_route_with_multiple_fibs_on_same_subnet_inet6_body()
@@ -315,7 +307,6 @@ same_ip_multiple_ifaces_fib0_head()
{
atf_set "descr" "Can remove an IPv4 alias from an interface when the same IPv4 is also assigned to another interface."
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
same_ip_multiple_ifaces_fib0_body()
{
@@ -329,6 +320,9 @@ same_ip_multiple_ifaces_fib0_body()
# Setup the interfaces, then remove one alias. It should not panic.
setup_tap 0 inet ${ADDR} ${MASK0}
TAP0=${TAP}
+ # After commit 361a8395f0b0e6f254fd138798232529679d99f6 it became
+ # an error to assign the same interface address twice.
+ atf_expect_fail "The test results in an ifconfig error and thus spuriously fails"
setup_tap 0 inet ${ADDR} ${MASK1}
TAP1=${TAP}
ifconfig ${TAP1} -alias ${ADDR}
@@ -358,18 +352,16 @@ same_ip_multiple_ifaces_head()
{
atf_set "descr" "Can remove an IPv4 alias from an interface when the same address is also assigned to another interface, on non-default FIBs."
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
same_ip_multiple_ifaces_body()
{
- atf_expect_fail "kern/189088 Assigning the same IP to multiple interfaces in different FIBs creates a host route for only one"
ADDR="192.0.2.2"
MASK0="24"
MASK1="32"
# Unlike most of the tests in this file, this is applicable regardless
# of net.add_addr_allfibs
- get_fibs 2
+ get_fibs 4
# Setup the interfaces, then remove one alias. It should not panic.
setup_tap ${FIB0} inet ${ADDR} ${MASK0}
@@ -381,13 +373,13 @@ same_ip_multiple_ifaces_body()
setfib ${FIB1} netstat -rn -f inet
# Do it again, in the opposite order. It should not panic.
- setup_tap ${FIB0} inet ${ADDR} ${MASK0}
+ setup_tap ${FIB2} inet ${ADDR} ${MASK0}
TAP0=${TAP}
- setup_tap ${FIB1} inet ${ADDR} ${MASK1}
+ setup_tap ${FIB3} inet ${ADDR} ${MASK1}
TAP1=${TAP}
ifconfig ${TAP0} -alias ${ADDR}
atf_check -o not-match:"^${ADDR}[[:space:]]" \
- setfib ${FIB0} netstat -rn -f inet
+ setfib ${FIB2} netstat -rn -f inet
}
same_ip_multiple_ifaces_cleanup()
{
@@ -404,7 +396,6 @@ same_ip_multiple_ifaces_inet6_head()
{
atf_set "descr" "Can remove an IPv6 alias from an interface when the same address is also assigned to another interface, on non-default FIBs."
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
same_ip_multiple_ifaces_inet6_body()
{
@@ -446,7 +437,7 @@ slaac_on_nondefault_fib6_head()
{
atf_set "descr" "SLAAC correctly installs routes on non-default FIBs"
atf_set "require.user" "root"
- atf_set "require.config" "fibs" "allow_sysctl_side_effects"
+ atf_set "require.config" "allow_sysctl_side_effects"
}
slaac_on_nondefault_fib6_body()
{
@@ -533,7 +524,6 @@ subnet_route_with_multiple_fibs_on_same_subnet_head()
{
atf_set "descr" "Multiple FIBs can have IPv4 subnet routes for the same subnet"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
subnet_route_with_multiple_fibs_on_same_subnet_body()
@@ -570,7 +560,6 @@ subnet_route_with_multiple_fibs_on_same_subnet_inet6_head()
{
atf_set "descr" "Multiple FIBs can have IPv6 subnet routes for the same subnet"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
subnet_route_with_multiple_fibs_on_same_subnet_inet6_body()
@@ -620,7 +609,6 @@ udp_dontroute_head()
{
atf_set "descr" "Source address selection for UDP packets with SO_DONTROUTE on non-default FIBs works"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
udp_dontroute_body()
@@ -671,7 +659,6 @@ udp_dontroute6_head()
{
atf_set "descr" "Source address selection for UDP IPv6 packets with SO_DONTROUTE on non-default FIBs works"
atf_set "require.user" "root"
- atf_set "require.config" "fibs"
}
udp_dontroute6_body()
@@ -748,15 +735,13 @@ get_fibs()
{
NUMFIBS=$1
net_fibs=`sysctl -n net.fibs`
+ if [ $net_fibs -lt $(($NUMFIBS + 1)) ]; then
+ atf_check -o ignore sysctl net.fibs=$(($NUMFIBS + 1))
+ net_fibs=`sysctl -n net.fibs`
+ fi
i=0
while [ $i -lt "$NUMFIBS" ]; do
- fib=`atf_config_get "fibs" | \
- awk -v i=$(( i + 1 )) '{print $i}'`
- echo "fib is ${fib}"
- eval FIB${i}=${fib}
- if [ "$fib" -ge "$net_fibs" ]; then
- atf_skip "The ${i}th configured fib is ${fib}, which is not less than net.fibs, which is ${net_fibs}"
- fi
+ eval FIB${i}=$(($i + 1))
i=$(( $i + 1 ))
done
}
@@ -816,9 +801,7 @@ setup_iface()
local ADDR=$4
local MASK=$5
local FLAGS=$6
- echo setfib ${FIB} \
- ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS
- setfib ${FIB} ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS
+ atf_check setfib ${FIB} ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS
}
# Create a tap(4) interface, configure it, and register it for cleanup.
diff --git a/tests/sys/netinet/forward.sh b/tests/sys/netinet/forward.sh
index e16927a27d07..be69e91b6137 100755
--- a/tests/sys/netinet/forward.sh
+++ b/tests/sys/netinet/forward.sh
@@ -34,7 +34,7 @@ fwd_ip_icmp_iface_fast_success_head() {
atf_set descr 'Test valid IPv4 on-stick fastforwarding to iface'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip_icmp_iface_fast_success_body() {
@@ -90,7 +90,7 @@ fwd_ip_icmp_gw_fast_success_head() {
atf_set descr 'Test valid IPv4 on-stick fastforwarding to gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip_icmp_gw_fast_success_body() {
@@ -150,7 +150,7 @@ fwd_ip_icmp_iface_slow_success_head() {
atf_set descr 'Test valid IPv4 on-stick "slow" forwarding to iface'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip_icmp_iface_slow_success_body() {
@@ -204,7 +204,7 @@ fwd_ip_icmp_gw_slow_success_head() {
atf_set descr 'Test valid IPv4 on-stick "slow" forwarding to gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip_icmp_gw_slow_success_body() {
@@ -259,6 +259,58 @@ fwd_ip_icmp_gw_slow_success_cleanup() {
vnet_cleanup
}
+atf_test_case "fwd_ip_blackhole" "cleanup"
+fwd_ip_blackhole_head() {
+
+ atf_set descr 'Test blackhole routes'
+ atf_set require.user root
+}
+
+fwd_ip_blackhole_body() {
+ jname="v4t-fwd_ip_blackhole"
+
+ vnet_init
+
+ epair=$(vnet_mkepair)
+ epair_out=$(vnet_mkepair)
+
+ ifconfig ${epair}a 192.0.2.2/24 up
+
+ vnet_mkjail ${jname} ${epair}b ${epair_out}b
+ jexec ${jname} ifconfig lo0 127.0.0.1/8 up
+ jexec ${jname} ifconfig ${epair}b 192.0.2.1/24 up
+ jexec ${jname} ifconfig ${epair_out}b 198.51.100.1/24 up
+ jexec ${jname} sysctl net.inet.ip.forwarding=1
+
+ route add default 192.0.2.1
+
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 -t 1 198.51.100.2
+ atf_check -s exit:0 -o match:"0 packets not forwardable" \
+ jexec ${jname} netstat -s -p ip
+
+ # Create blackhole route
+ jexec ${jname} /sbin/route add 198.51.100.2 -blackhole -fib 0
+ jexec ${jname} netstat -rn
+
+ # Include an IP option to ensure slow path
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 -t 1 -R 198.51.100.2
+ atf_check -s exit:0 -o match:"1 packet not forwardable" \
+ jexec ${jname} netstat -s -p ip
+
+ # Now try via the fast path
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 -t 1 198.51.100.2
+ atf_check -s exit:0 -o match:"2 packets not forwardable" \
+ jexec ${jname} netstat -s -p ip
+}
+
+fwd_ip_blackhole_cleanup() {
+
+ vnet_cleanup
+}
+
atf_init_test_cases()
{
@@ -266,6 +318,7 @@ atf_init_test_cases()
atf_add_test_case "fwd_ip_icmp_gw_fast_success"
atf_add_test_case "fwd_ip_icmp_iface_slow_success"
atf_add_test_case "fwd_ip_icmp_gw_slow_success"
+ atf_add_test_case "fwd_ip_blackhole"
}
# end
diff --git a/tests/sys/netinet/igmp.py b/tests/sys/netinet/igmp.py
index b079c5d18664..feb9b8b571d5 100644
--- a/tests/sys/netinet/igmp.py
+++ b/tests/sys/netinet/igmp.py
@@ -36,7 +36,6 @@ logging.getLogger("scapy").setLevel(logging.CRITICAL)
curdir = os.path.dirname(os.path.realpath(__file__))
netpfil_common = curdir + "/../netpfil/common"
sys.path.append(netpfil_common)
-from sniffer import Sniffer
sc = None
sp = None
@@ -63,6 +62,25 @@ def check_igmpv3(args, pkt):
return True
+def check_igmpv2(args, pkt):
+ pkt.show()
+
+ igmp = pkt.getlayer(sc.igmp.IGMP)
+ if igmp is None:
+ return False
+
+ if igmp.gaddr != args["group"]:
+ return False
+
+ if args["type"] == "join":
+ if igmp.type != 0x16:
+ return False
+ if args["type"] == "leave":
+ if igmp.type != 0x17:
+ return False
+
+ return True
+
class TestIGMP(VnetTestTemplate):
REQUIRED_MODULES = []
TOPOLOGY = {
@@ -81,12 +99,14 @@ class TestIGMP(VnetTestTemplate):
sp = _sp
super().setup_method(method)
+ @pytest.mark.require_progs(["scapy"])
def test_igmp3_join_leave(self):
- "Test that we send the expected join/leave IGMPv2 messages"
+ "Test that we send the expected join/leave IGMPv3 messages"
if1 = self.vnet.iface_alias_map["if1"]
# Start a background sniff
+ from sniffer import Sniffer
expected_pkt = { "type": "join", "group": "230.0.0.1" }
sniffer = Sniffer(expected_pkt, check_igmpv3, if1.name, timeout=10)
@@ -106,3 +126,32 @@ class TestIGMP(VnetTestTemplate):
s.close()
sniffer.join()
assert(sniffer.correctPackets > 0)
+
+ @pytest.mark.require_progs(["scapy"])
+ def test_igmp2_join_leave(self):
+ "Test that we send the expected join/leave IGMPv2 messages"
+ ToolsHelper.print_output("/sbin/sysctl net.inet.igmp.default_version=2")
+
+ if1 = self.vnet.iface_alias_map["if1"]
+
+ # Start a background sniff
+ from sniffer import Sniffer
+ expected_pkt = { "type": "join", "group": "230.0.0.1" }
+ sniffer = Sniffer(expected_pkt, check_igmpv2, if1.name, timeout=10)
+
+ # Now join a multicast group, and see if we're getting the igmp packet we expect
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
+ mreq = struct.pack("4sl", socket.inet_aton('230.0.0.1'), socket.INADDR_ANY)
+ s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
+
+ # Wait for the sniffer to see the join packet
+ sniffer.join()
+ assert(sniffer.correctPackets > 0)
+
+ # Now leave, check for the packet
+ expected_pkt = { "type": "leave", "group": "230.0.0.1" }
+ sniffer = Sniffer(expected_pkt, check_igmpv2, if1.name)
+
+ s.close()
+ sniffer.join()
+ assert(sniffer.correctPackets > 0)
diff --git a/tests/sys/netinet/ip_reass_test.c b/tests/sys/netinet/ip_reass_test.c
index a65bfa34e1d4..538815bd7a2c 100644
--- a/tests/sys/netinet/ip_reass_test.c
+++ b/tests/sys/netinet/ip_reass_test.c
@@ -60,12 +60,16 @@ update_cksum(struct ip *ip)
{
size_t i;
uint32_t cksum;
- uint16_t *cksump;
+ uint8_t *cksump;
+ uint16_t tmp;
ip->ip_sum = 0;
- cksump = (uint16_t *)ip;
- for (cksum = 0, i = 0; i < sizeof(*ip) / sizeof(*cksump); cksump++, i++)
- cksum += ntohs(*cksump);
+ cksump = (char *)ip;
+ for (cksum = 0, i = 0; i < sizeof(*ip) / sizeof(uint16_t); i++) {
+ tmp = *cksump++;
+ tmp = tmp << 8 | *cksump++;
+ cksum += ntohs(tmp);
+ }
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum = ~(cksum + (cksum >> 16));
ip->ip_sum = htons((uint16_t)cksum);
diff --git a/tests/sys/netinet/libalias/2_natout.c b/tests/sys/netinet/libalias/2_natout.c
index c6f5797b2db7..24ca06d11bf4 100644
--- a/tests/sys/netinet/libalias/2_natout.c
+++ b/tests/sys/netinet/libalias/2_natout.c
@@ -359,6 +359,172 @@ ATF_TC_BODY(8_portrange, dummy)
LibAliasUninit(la);
}
+ATF_TC_WITHOUT_HEAD(9_udp_eim_mapping);
+ATF_TC_BODY(9_udp_eim_mapping, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *po2, *po3;
+ struct udphdr *uo, *uo2, *uo3;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t dport2 = 0x6789;
+ uint16_t aport, aport2, aport3;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0);
+
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+
+ /* Change of dst port shouldn't change alias port */
+ po2 = ip_packet(0, 64);
+ UDP_NAT_CHECK(po2, uo2, prv1, sport, ext, dport2, masq);
+ aport2 = ntohs(uo2->uh_sport);
+ ATF_CHECK_EQ_MSG(aport, aport2,
+ "NAT uses address- and port-dependent mapping (%uh -> %uh)",
+ aport, aport2);
+
+ /* Change of dst address shouldn't change alias port */
+ po3 = ip_packet(0, 64);
+ UDP_NAT_CHECK(po3, uo3, prv1, sport, pub, dport, masq);
+ aport3 = ntohs(uo3->uh_sport);
+ ATF_CHECK_EQ_MSG(aport, aport3, "NAT uses address-dependent mapping");
+
+ free(po);
+ free(po2);
+ free(po3);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(10_udp_eim_out_in);
+ATF_TC_BODY(10_udp_eim_out_in, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *po2, *po3;
+ struct udphdr *uo, *uo2, *uo3;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t dport2 = 0x6789;
+ uint16_t aport;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0);
+
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport, pub, dport, masq);
+ aport = ntohs(uo->uh_sport);
+
+ /* Accepts inbound packets from different port */
+ po2 = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(po2, uo2, pub, dport2, masq, aport, prv1, sport);
+
+ /* Accepts inbound packets from differerent host and port */
+ po3 = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(po3, uo3, pub2, dport2, masq, aport, prv1, sport);
+
+ free(po);
+ free(po2);
+ free(po3);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(11_udp_eim_with_deny_incoming);
+ATF_TC_BODY(11_udp_eim_with_deny_incoming, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *po2, *po3, *po4;
+ struct udphdr *uo;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t dport2 = 0x6789;
+ uint16_t aport;
+ int ret;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la,
+ PKT_ALIAS_UDP_EIM | PKT_ALIAS_DENY_INCOMING,
+ ~0);
+
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport, pub, dport, masq);
+ aport = ntohs(uo->uh_sport);
+
+ po2 = ip_packet(0, 64);
+ po2->ip_src = pub;
+ po2->ip_dst = masq;
+ set_udp(po2, dport, aport);
+ ret = LibAliasIn(la, po2, 64);
+ ATF_CHECK_EQ_MSG(PKT_ALIAS_OK, ret,
+ "LibAliasIn failed with error %d\n", ret);
+
+ po3 = ip_packet(0, 64);
+ po3->ip_src = pub;
+ po3->ip_dst = masq;
+ set_udp(po3, dport2, aport);
+ ret = LibAliasIn(la, po3, 64);
+ ATF_CHECK_EQ_MSG(PKT_ALIAS_IGNORED, ret,
+ "incoming packet from different port not ignored "
+ "with PKT_ALIAS_DENY_INCOMING");
+
+ po4 = ip_packet(0, 64);
+ po4->ip_src = pub2;
+ po4->ip_dst = masq;
+ set_udp(po4, dport2, aport);
+ ret = LibAliasIn(la, po4, 64);
+ ATF_CHECK_EQ_MSG(PKT_ALIAS_IGNORED, ret,
+ "incoming packet from different address and port not ignored "
+ "with PKT_ALIAS_DENY_INCOMING");
+
+ free(po);
+ free(po2);
+ free(po3);
+ free(po4);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(12_udp_eim_hairpinning);
+ATF_TC_BODY(12_udp_eim_hairpinning, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *po2, *po3;
+ struct udphdr *uo, *uo2, *uo3;
+ uint16_t sport1 = 0x1234;
+ uint16_t sport2 = 0x2345;
+ uint16_t dport = 0x5678;
+ uint16_t extport1, extport2;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_UDP_EIM, ~0);
+
+ /* prv1 sends out somewhere (eg. a STUN server) */
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport1, pub, dport, masq);
+ extport1 = ntohs(uo->uh_sport);
+
+ /* prv2, behind the same NAT as prv1, also sends out somewhere */
+ po2 = ip_packet(0, 64);
+ UDP_NAT_CHECK(po2, uo2, prv2, sport2, pub, dport, masq);
+ extport2 = ntohs(uo2->uh_sport);
+
+ /* hairpin: prv1 sends to prv2's external NAT mapping
+ * (unaware it could address it internally instead).
+ */
+ po3 = ip_packet(0, 64);
+ UDP_NAT_CHECK(po3, uo3, prv1, sport1, masq, extport2, masq);
+ UDP_UNNAT_CHECK(po3, uo3, masq, extport1, masq, extport2,
+ prv2, sport2);
+
+ free(po);
+ free(po2);
+ free(po3);
+ LibAliasUninit(la);
+}
+
ATF_TP_ADD_TCS(natout)
{
/* Use "dd if=/dev/random bs=2 count=1 | od -x" to reproduce */
@@ -372,6 +538,10 @@ ATF_TP_ADD_TCS(natout)
ATF_TP_ADD_TC(natout, 6_cleartable);
ATF_TP_ADD_TC(natout, 7_stress);
ATF_TP_ADD_TC(natout, 8_portrange);
+ ATF_TP_ADD_TC(natout, 9_udp_eim_mapping);
+ ATF_TP_ADD_TC(natout, 10_udp_eim_out_in);
+ ATF_TP_ADD_TC(natout, 11_udp_eim_with_deny_incoming);
+ ATF_TP_ADD_TC(natout, 12_udp_eim_hairpinning);
return atf_no_error();
}
diff --git a/tests/sys/netinet/libalias/Makefile b/tests/sys/netinet/libalias/Makefile
index 20d904511538..43bef996fae7 100644
--- a/tests/sys/netinet/libalias/Makefile
+++ b/tests/sys/netinet/libalias/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netinet/libalias
diff --git a/tests/sys/netinet/libalias/util.c b/tests/sys/netinet/libalias/util.c
index 14ba196a59a5..8ceb8355c8ff 100644
--- a/tests/sys/netinet/libalias/util.c
+++ b/tests/sys/netinet/libalias/util.c
@@ -41,6 +41,7 @@
/* common ip ranges */
struct in_addr masq = { htonl(0x01020304) };
struct in_addr pub = { htonl(0x0102dead) };
+struct in_addr pub2 = { htonl(0x0102beef) };
struct in_addr prv1 = { htonl(0x0a00dead) };
struct in_addr prv2 = { htonl(0xac10dead) };
struct in_addr prv3 = { htonl(0xc0a8dead) };
diff --git a/tests/sys/netinet/libalias/util.h b/tests/sys/netinet/libalias/util.h
index 786e48e41f37..f58a1ad26248 100644
--- a/tests/sys/netinet/libalias/util.h
+++ b/tests/sys/netinet/libalias/util.h
@@ -41,7 +41,7 @@
#define _UTIL_H
/* common ip ranges */
-extern struct in_addr masq, pub, prv1, prv2, prv3, cgn, ext, ANY_ADDR;
+extern struct in_addr masq, pub, pub2, prv1, prv2, prv3, cgn, ext, ANY_ADDR;
int randcmp(const void *a, const void *b);
void hexdump(void *p, size_t len);
diff --git a/tests/sys/netinet/multicast.sh b/tests/sys/netinet/multicast.sh
new file mode 100644
index 000000000000..eb2b962dac70
--- /dev/null
+++ b/tests/sys/netinet/multicast.sh
@@ -0,0 +1,61 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/../common/vnet.subr
+
+# See regression fixed in baad45c9c12028964acd0b58096f3aaa0fb22859
+atf_test_case "IP_MULTICAST_IF" "cleanup"
+IP_MULTICAST_IF_head()
+{
+ atf_set descr \
+ 'sendto() for IP_MULTICAST_IF socket does not do routing lookup'
+ atf_set require.user root
+
+}
+
+IP_MULTICAST_IF_body()
+{
+ local epair mjail
+
+ vnet_init
+ # The test doesn't use our half of epair
+ epair=$(vnet_mkepair)
+ vnet_mkjail mjail ${epair}a
+ jexec mjail ifconfig ${epair}a up
+ jexec mjail ifconfig ${epair}a 192.0.2.1/24
+ atf_check -s exit:0 -o empty \
+ jexec mjail $(atf_get_srcdir)/sendto-IP_MULTICAST_IF 192.0.2.1
+}
+
+IP_MULTICAST_IF_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "IP_MULTICAST_IF"
+}
diff --git a/tests/sys/netinet/redirect.sh b/tests/sys/netinet/redirect.sh
index 4a069d515bb0..ad5b562da57a 100755
--- a/tests/sys/netinet/redirect.sh
+++ b/tests/sys/netinet/redirect.sh
@@ -34,7 +34,7 @@ valid_redirect_head() {
atf_set descr 'Test valid IPv4 redirect'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
valid_redirect_body() {
diff --git a/tests/sys/netinet/sendto-IP_MULTICAST_IF.c b/tests/sys/netinet/sendto-IP_MULTICAST_IF.c
new file mode 100644
index 000000000000..d478e4da0b3b
--- /dev/null
+++ b/tests/sys/netinet/sendto-IP_MULTICAST_IF.c
@@ -0,0 +1,63 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <assert.h>
+#include <err.h>
+
+int
+main(int argc, char *argv[])
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ struct in_addr in;
+ int s, rv;
+
+ if (argc < 2)
+ errx(1, "Usage: %s IPv4-address", argv[0]);
+
+ if (inet_pton(AF_INET, argv[1], &in) != 1)
+ err(1, "inet_pton(%s) failed", argv[1]);
+
+ assert((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ assert(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ assert(setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &in, sizeof(in))
+ == 0);
+ /* RFC 6676 */
+ assert(inet_pton(AF_INET, "233.252.0.1", &sin.sin_addr) == 1);
+ sin.sin_port = htons(6676);
+ rv = sendto(s, &sin, sizeof(sin), 0,
+ (struct sockaddr *)&sin, sizeof(sin));
+ if (rv != sizeof(sin))
+ err(1, "sendto failed");
+
+ return (0);
+}
diff --git a/tests/sys/netinet/so_reuseport_lb_test.c b/tests/sys/netinet/so_reuseport_lb_test.c
index 64fe0b53618d..fa9d6e425884 100644
--- a/tests/sys/netinet/so_reuseport_lb_test.c
+++ b/tests/sys/netinet/so_reuseport_lb_test.c
@@ -28,12 +28,16 @@
*/
#include <sys/param.h>
+#include <sys/event.h>
#include <sys/socket.h>
#include <netinet/in.h>
+#include <netinet/tcp.h>
#include <err.h>
#include <errno.h>
+#include <pthread.h>
+#include <stdatomic.h>
#include <stdlib.h>
#include <unistd.h>
@@ -235,10 +239,328 @@ ATF_TC_BODY(basic_ipv6, tc)
}
}
+struct concurrent_add_softc {
+ struct sockaddr_storage ss;
+ int socks[128];
+ int kq;
+};
+
+static void *
+listener(void *arg)
+{
+ for (struct concurrent_add_softc *sc = arg;;) {
+ struct kevent kev;
+ ssize_t n;
+ int error, count, cs, s;
+ uint8_t b;
+
+ count = kevent(sc->kq, NULL, 0, &kev, 1, NULL);
+ ATF_REQUIRE_MSG(count == 1,
+ "kevent() failed: %s", strerror(errno));
+
+ s = (int)kev.ident;
+ cs = accept(s, NULL, NULL);
+ ATF_REQUIRE_MSG(cs >= 0,
+ "accept() failed: %s", strerror(errno));
+
+ b = 'M';
+ n = write(cs, &b, sizeof(b));
+ ATF_REQUIRE_MSG(n >= 0, "write() failed: %s", strerror(errno));
+ ATF_REQUIRE(n == 1);
+
+ error = close(cs);
+ ATF_REQUIRE_MSG(error == 0 || errno == ECONNRESET,
+ "close() failed: %s", strerror(errno));
+ }
+}
+
+static void *
+connector(void *arg)
+{
+ for (struct concurrent_add_softc *sc = arg;;) {
+ ssize_t n;
+ int error, s;
+ uint8_t b;
+
+ s = socket(sc->ss.ss_family, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(s >= 0, "socket() failed: %s", strerror(errno));
+
+ error = setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (int[]){1},
+ sizeof(int));
+
+ error = connect(s, (struct sockaddr *)&sc->ss, sc->ss.ss_len);
+ ATF_REQUIRE_MSG(error == 0, "connect() failed: %s",
+ strerror(errno));
+
+ n = read(s, &b, sizeof(b));
+ ATF_REQUIRE_MSG(n >= 0, "read() failed: %s",
+ strerror(errno));
+ ATF_REQUIRE(n == 1);
+ ATF_REQUIRE(b == 'M');
+ error = close(s);
+ ATF_REQUIRE_MSG(error == 0,
+ "close() failed: %s", strerror(errno));
+ }
+}
+
+/*
+ * Run three threads. One accepts connections from listening sockets on a
+ * kqueue, while the other makes connections. The third thread slowly adds
+ * sockets to the LB group. This is meant to help flush out race conditions.
+ */
+ATF_TC_WITHOUT_HEAD(concurrent_add);
+ATF_TC_BODY(concurrent_add, tc)
+{
+ struct concurrent_add_softc sc;
+ struct sockaddr_in *sin;
+ pthread_t threads[4];
+ int error;
+
+ sc.kq = kqueue();
+ ATF_REQUIRE_MSG(sc.kq >= 0, "kqueue() failed: %s", strerror(errno));
+
+ error = pthread_create(&threads[0], NULL, listener, &sc);
+ ATF_REQUIRE_MSG(error == 0, "pthread_create() failed: %s",
+ strerror(error));
+
+ sin = (struct sockaddr_in *)&sc.ss;
+ memset(sin, 0, sizeof(*sin));
+ sin->sin_len = sizeof(*sin);
+ sin->sin_family = AF_INET;
+ sin->sin_port = htons(0);
+ sin->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ for (size_t i = 0; i < nitems(sc.socks); i++) {
+ struct kevent kev;
+ int s;
+
+ sc.socks[i] = s = socket(AF_INET, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(s >= 0, "socket() failed: %s", strerror(errno));
+
+ error = setsockopt(s, SOL_SOCKET, SO_REUSEPORT_LB, (int[]){1},
+ sizeof(int));
+ ATF_REQUIRE_MSG(error == 0,
+ "setsockopt(SO_REUSEPORT_LB) failed: %s", strerror(errno));
+
+ error = bind(s, (struct sockaddr *)sin, sizeof(*sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s",
+ strerror(errno));
+
+ error = listen(s, 5);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s",
+ strerror(errno));
+
+ EV_SET(&kev, s, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
+ error = kevent(sc.kq, &kev, 1, NULL, 0, NULL);
+ ATF_REQUIRE_MSG(error == 0, "kevent() failed: %s",
+ strerror(errno));
+
+ if (i == 0) {
+ socklen_t slen = sizeof(sc.ss);
+
+ error = getsockname(sc.socks[i],
+ (struct sockaddr *)&sc.ss, &slen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s",
+ strerror(errno));
+ ATF_REQUIRE(sc.ss.ss_family == AF_INET);
+
+ for (size_t j = 1; j < nitems(threads); j++) {
+ error = pthread_create(&threads[j], NULL,
+ connector, &sc);
+ ATF_REQUIRE_MSG(error == 0,
+ "pthread_create() failed: %s",
+ strerror(error));
+ }
+ }
+
+ usleep(20000);
+ }
+}
+
+/*
+ * Try calling listen(2) twice on a socket with SO_REUSEPORT_LB set.
+ */
+ATF_TC_WITHOUT_HEAD(double_listen_ipv4);
+ATF_TC_BODY(double_listen_ipv4, tc)
+{
+ struct sockaddr_in sin;
+ int error, s;
+
+ s = lb_listen_socket(PF_INET, 0);
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_len = sizeof(sin);
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ error = bind(s, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = listen(s, 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+ error = listen(s, 2);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ error = close(s);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno));
+}
+
+/*
+ * Try calling listen(2) twice on a socket with SO_REUSEPORT_LB set.
+ */
+ATF_TC_WITHOUT_HEAD(double_listen_ipv6);
+ATF_TC_BODY(double_listen_ipv6, tc)
+{
+ struct sockaddr_in6 sin6;
+ int error, s;
+
+ s = lb_listen_socket(PF_INET6, 0);
+
+ memset(&sin6, 0, sizeof(sin6));
+ sin6.sin6_len = sizeof(sin6);
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_port = htons(0);
+ sin6.sin6_addr = in6addr_loopback;
+ error = bind(s, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = listen(s, 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+ error = listen(s, 2);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ error = close(s);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno));
+}
+
+/*
+ * Try binding many sockets to the same lbgroup without calling listen(2) on
+ * them.
+ */
+ATF_TC_WITHOUT_HEAD(bind_without_listen);
+ATF_TC_BODY(bind_without_listen, tc)
+{
+ const int nsockets = 100;
+ struct sockaddr_in sin;
+ socklen_t socklen;
+ int error, s, s2[nsockets];
+
+ s = lb_listen_socket(PF_INET, 0);
+
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_len = sizeof(sin);
+ sin.sin_family = AF_INET;
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ error = bind(s, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ socklen = sizeof(sin);
+ error = getsockname(s, (struct sockaddr *)&sin, &socklen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s",
+ strerror(errno));
+
+ for (int i = 0; i < nsockets; i++) {
+ s2[i] = lb_listen_socket(PF_INET, 0);
+ error = bind(s2[i], (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+ }
+ for (int i = 0; i < nsockets; i++) {
+ error = listen(s2[i], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+ }
+ for (int i = 0; i < nsockets; i++) {
+ error = close(s2[i]);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno));
+ }
+
+ error = close(s);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s", strerror(errno));
+}
+
+/*
+ * Check that SO_REUSEPORT_LB doesn't mess with connect(2).
+ * Two sockets:
+ * 1) auxiliary peer socket 'p', where we connect to
+ * 2) test socket 's', that sets SO_REUSEPORT_LB and then connect(2)s to 'p'
+ */
+ATF_TC_WITHOUT_HEAD(connect_not_bound);
+ATF_TC_BODY(connect_not_bound, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(sin),
+ .sin_addr = { htonl(INADDR_LOOPBACK) },
+ };
+ socklen_t slen = sizeof(struct sockaddr_in);
+ int p, s, rv;
+
+ ATF_REQUIRE((p = socket(PF_INET, SOCK_STREAM, 0)) > 0);
+ ATF_REQUIRE(bind(p, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(listen(p, 1) == 0);
+ ATF_REQUIRE(getsockname(p, (struct sockaddr *)&sin, &slen) == 0);
+
+ s = lb_listen_socket(PF_INET, 0);
+ rv = connect(s, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP,
+ "Expected EOPNOTSUPP on connect(2) not met. Got %d, errno %d",
+ rv, errno);
+ rv = sendto(s, "test", 4, 0, (struct sockaddr *)&sin,
+ sizeof(sin));
+ ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP,
+ "Expected EOPNOTSUPP on sendto(2) not met. Got %d, errno %d",
+ rv, errno);
+
+ close(p);
+ close(s);
+}
+
+/*
+ * Same as above, but we also bind(2) between setsockopt(2) of SO_REUSEPORT_LB
+ * and the connect(2).
+ */
+ATF_TC_WITHOUT_HEAD(connect_bound);
+ATF_TC_BODY(connect_bound, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(sin),
+ .sin_addr = { htonl(INADDR_LOOPBACK) },
+ };
+ socklen_t slen = sizeof(struct sockaddr_in);
+ int p, s, rv;
+
+ ATF_REQUIRE((p = socket(PF_INET, SOCK_STREAM, 0)) > 0);
+ ATF_REQUIRE(bind(p, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(listen(p, 1) == 0);
+
+ s = lb_listen_socket(PF_INET, 0);
+ ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(getsockname(p, (struct sockaddr *)&sin, &slen) == 0);
+ rv = connect(s, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP,
+ "Expected EOPNOTSUPP on connect(2) not met. Got %d, errno %d",
+ rv, errno);
+ rv = sendto(s, "test", 4, 0, (struct sockaddr *)&sin,
+ sizeof(sin));
+ ATF_REQUIRE_MSG(rv == -1 && errno == EOPNOTSUPP,
+ "Expected EOPNOTSUPP on sendto(2) not met. Got %d, errno %d",
+ rv, errno);
+
+ close(p);
+ close(s);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, basic_ipv4);
ATF_TP_ADD_TC(tp, basic_ipv6);
+ ATF_TP_ADD_TC(tp, concurrent_add);
+ ATF_TP_ADD_TC(tp, double_listen_ipv4);
+ ATF_TP_ADD_TC(tp, double_listen_ipv6);
+ ATF_TP_ADD_TC(tp, bind_without_listen);
+ ATF_TP_ADD_TC(tp, connect_not_bound);
+ ATF_TP_ADD_TC(tp, connect_bound);
return (atf_no_error());
}
diff --git a/tests/sys/netinet/socket_afinet.c b/tests/sys/netinet/socket_afinet.c
index 7076f084719a..9c718fc5a901 100644
--- a/tests/sys/netinet/socket_afinet.c
+++ b/tests/sys/netinet/socket_afinet.c
@@ -2,6 +2,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*
* Copyright (c) 2019 Bjoern A. Zeeb
+ * Copyright (c) 2024 Stormshield
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
@@ -25,11 +26,17 @@
* SUCH DAMAGE.
*/
-#include <sys/cdefs.h>
+#include <sys/param.h>
#include <sys/socket.h>
+#include <sys/wait.h>
+
#include <netinet/in.h>
+
#include <errno.h>
#include <poll.h>
+#include <pwd.h>
+#include <stdio.h>
+#include <unistd.h>
#include <atf-c.h>
@@ -281,6 +288,301 @@ ATF_TC_BODY(socket_afinet_stream_reconnect, tc)
ATF_CHECK_EQ(0, rc);
}
+/*
+ * Make sure that unprivileged users can't set the IP_BINDANY or IPV6_BINDANY
+ * socket options.
+ */
+ATF_TC(socket_afinet_bindany);
+ATF_TC_HEAD(socket_afinet_bindany, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "unprivileged");
+}
+ATF_TC_BODY(socket_afinet_bindany, tc)
+{
+ int s;
+
+ s = socket(AF_INET, SOCK_STREAM, 0);
+ ATF_REQUIRE(s >= 0);
+ ATF_REQUIRE_ERRNO(EPERM,
+ setsockopt(s, IPPROTO_IP, IP_BINDANY, &(int){1}, sizeof(int)) ==
+ -1);
+ ATF_REQUIRE(close(s) == 0);
+
+ s = socket(AF_INET, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s >= 0);
+ ATF_REQUIRE_ERRNO(EPERM,
+ setsockopt(s, IPPROTO_IP, IP_BINDANY, &(int){1}, sizeof(int)) ==
+ -1);
+ ATF_REQUIRE(close(s) == 0);
+
+ s = socket(AF_INET6, SOCK_STREAM, 0);
+ ATF_REQUIRE(s >= 0);
+ ATF_REQUIRE_ERRNO(EPERM,
+ setsockopt(s, IPPROTO_IPV6, IPV6_BINDANY, &(int){1}, sizeof(int)) ==
+ -1);
+ ATF_REQUIRE(close(s) == 0);
+
+ s = socket(AF_INET6, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s >= 0);
+ ATF_REQUIRE_ERRNO(EPERM,
+ setsockopt(s, IPPROTO_IPV6, IPV6_BINDANY, &(int){1}, sizeof(int)) ==
+ -1);
+ ATF_REQUIRE(close(s) == 0);
+}
+
+/*
+ * Bind a socket to the specified address, optionally dropping privileges and
+ * setting one of the SO_REUSE* options first.
+ *
+ * Returns true if the bind succeeded, and false if it failed with EADDRINUSE.
+ */
+static bool
+child_bind(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt,
+ bool unpriv)
+{
+ const char *user;
+ pid_t child;
+
+ if (unpriv) {
+ if (!atf_tc_has_config_var(tc, "unprivileged_user"))
+ atf_tc_skip("unprivileged_user not set");
+ user = atf_tc_get_config_var(tc, "unprivileged_user");
+ } else {
+ user = NULL;
+ }
+
+ child = fork();
+ ATF_REQUIRE(child != -1);
+ if (child == 0) {
+ int s;
+
+ if (user != NULL) {
+ struct passwd *passwd;
+
+ passwd = getpwnam(user);
+ if (seteuid(passwd->pw_uid) != 0)
+ _exit(1);
+ }
+
+ s = socket(sa->sa_family, type, 0);
+ if (s < 0)
+ _exit(2);
+ if (bind(s, sa, sa->sa_len) == 0)
+ _exit(3);
+ if (errno != EADDRINUSE)
+ _exit(4);
+ if (opt != 0) {
+ if (setsockopt(s, SOL_SOCKET, opt, &(int){1},
+ sizeof(int)) != 0)
+ _exit(5);
+ }
+ if (bind(s, sa, sa->sa_len) == 0)
+ _exit(6);
+ if (errno != EADDRINUSE)
+ _exit(7);
+ _exit(0);
+ } else {
+ int status;
+
+ ATF_REQUIRE_EQ(waitpid(child, &status, 0), child);
+ ATF_REQUIRE(WIFEXITED(status));
+ status = WEXITSTATUS(status);
+ ATF_REQUIRE_MSG(status == 0 || status == 6,
+ "child exited with %d", status);
+ return (status == 6);
+ }
+}
+
+static bool
+child_bind_priv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt)
+{
+ return (child_bind(tc, type, sa, opt, false));
+}
+
+static bool
+child_bind_unpriv(const atf_tc_t *tc, int type, struct sockaddr *sa, int opt)
+{
+ return (child_bind(tc, type, sa, opt, true));
+}
+
+static int
+bind_socket(int domain, int type, int opt, bool unspec, struct sockaddr *sa)
+{
+ socklen_t slen;
+ int s;
+
+ s = socket(domain, type, 0);
+ ATF_REQUIRE(s >= 0);
+
+ if (domain == AF_INET) {
+ struct sockaddr_in sin;
+
+ bzero(&sin, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_addr.s_addr = htonl(unspec ?
+ INADDR_ANY : INADDR_LOOPBACK);
+ sin.sin_port = htons(0);
+ ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+
+ slen = sizeof(sin);
+ } else /* if (domain == AF_INET6) */ {
+ struct sockaddr_in6 sin6;
+
+ bzero(&sin6, sizeof(sin6));
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_len = sizeof(sin6);
+ sin6.sin6_addr = unspec ? in6addr_any : in6addr_loopback;
+ sin6.sin6_port = htons(0);
+ ATF_REQUIRE(bind(s, (struct sockaddr *)&sin6, sizeof(sin6)) == 0);
+
+ slen = sizeof(sin6);
+ }
+
+ if (opt != 0) {
+ ATF_REQUIRE(setsockopt(s, SOL_SOCKET, opt, &(int){1},
+ sizeof(int)) == 0);
+ }
+
+ ATF_REQUIRE(getsockname(s, sa, &slen) == 0);
+
+ return (s);
+}
+
+static void
+multibind_test(const atf_tc_t *tc, int domain, int type)
+{
+ struct sockaddr_storage ss;
+ int opts[4] = { 0, SO_REUSEADDR, SO_REUSEPORT, SO_REUSEPORT_LB };
+ int s;
+ bool flags[2] = { false, true };
+ bool res;
+
+ for (size_t flagi = 0; flagi < nitems(flags); flagi++) {
+ for (size_t opti = 0; opti < nitems(opts); opti++) {
+ s = bind_socket(domain, type, opts[opti], flags[flagi],
+ (struct sockaddr *)&ss);
+ for (size_t optj = 0; optj < nitems(opts); optj++) {
+ int opt;
+
+ opt = opts[optj];
+ res = child_bind_priv(tc, type,
+ (struct sockaddr *)&ss, opt);
+ /*
+ * Multi-binding is only allowed when both
+ * sockets have SO_REUSEPORT or SO_REUSEPORT_LB
+ * set.
+ */
+ if (opts[opti] != 0 &&
+ opts[opti] != SO_REUSEADDR && opti == optj)
+ ATF_REQUIRE(res);
+ else
+ ATF_REQUIRE(!res);
+
+ res = child_bind_unpriv(tc, type,
+ (struct sockaddr *)&ss, opt);
+ /*
+ * Multi-binding is only allowed when both
+ * sockets have the same owner.
+ */
+ ATF_REQUIRE(!res);
+ }
+ ATF_REQUIRE(close(s) == 0);
+ }
+ }
+}
+
+/*
+ * Try to bind two sockets to the same address/port tuple. Under some
+ * conditions this is permitted.
+ */
+ATF_TC(socket_afinet_multibind);
+ATF_TC_HEAD(socket_afinet_multibind, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "require.config", "unprivileged_user");
+}
+ATF_TC_BODY(socket_afinet_multibind, tc)
+{
+ multibind_test(tc, AF_INET, SOCK_STREAM);
+ multibind_test(tc, AF_INET, SOCK_DGRAM);
+ multibind_test(tc, AF_INET6, SOCK_STREAM);
+ multibind_test(tc, AF_INET6, SOCK_DGRAM);
+}
+
+static void
+bind_connected_port_test(const atf_tc_t *tc, int domain)
+{
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ struct sockaddr *sinp;
+ int error, sd[3], tmp;
+ bool res;
+
+ /*
+ * Create a connected socket pair.
+ */
+ sd[0] = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd[0] >= 0, "socket failed: %s", strerror(errno));
+ sd[1] = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd[1] >= 0, "socket failed: %s", strerror(errno));
+ if (domain == PF_INET) {
+ memset(&sin, 0, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_addr.s_addr = htonl(INADDR_ANY);
+ sin.sin_port = htons(0);
+ sinp = (struct sockaddr *)&sin;
+ } else {
+ ATF_REQUIRE(domain == PF_INET6);
+ memset(&sin6, 0, sizeof(sin6));
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_len = sizeof(sin6);
+ sin6.sin6_addr = in6addr_any;
+ sin6.sin6_port = htons(0);
+ sinp = (struct sockaddr *)&sin6;
+ }
+
+ error = bind(sd[0], sinp, sinp->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "bind failed: %s", strerror(errno));
+ error = listen(sd[0], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen failed: %s", strerror(errno));
+
+ error = getsockname(sd[0], sinp, &(socklen_t){ sinp->sa_len });
+ ATF_REQUIRE_MSG(error == 0, "getsockname failed: %s", strerror(errno));
+ if (domain == PF_INET)
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ error = connect(sd[1], sinp, sinp->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "connect failed: %s", strerror(errno));
+ tmp = accept(sd[0], NULL, NULL);
+ ATF_REQUIRE_MSG(tmp >= 0, "accept failed: %s", strerror(errno));
+ ATF_REQUIRE(close(sd[0]) == 0);
+ sd[0] = tmp;
+
+ /* bind() should succeed even from an unprivileged user. */
+ res = child_bind(tc, SOCK_STREAM, sinp, 0, false);
+ ATF_REQUIRE(!res);
+ res = child_bind(tc, SOCK_STREAM, sinp, 0, true);
+ ATF_REQUIRE(!res);
+}
+
+/*
+ * Normally bind() prevents port stealing by a different user, even when
+ * SO_REUSE* are specified. However, if the port is bound by a connected
+ * socket, then it's fair game.
+ */
+ATF_TC(socket_afinet_bind_connected_port);
+ATF_TC_HEAD(socket_afinet_bind_connected_port, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "require.config", "unprivileged_user");
+}
+ATF_TC_BODY(socket_afinet_bind_connected_port, tc)
+{
+ bind_connected_port_test(tc, AF_INET);
+ bind_connected_port_test(tc, AF_INET6);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, socket_afinet);
@@ -289,6 +591,9 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, socket_afinet_poll_no_rdhup);
ATF_TP_ADD_TC(tp, socket_afinet_poll_rdhup);
ATF_TP_ADD_TC(tp, socket_afinet_stream_reconnect);
+ ATF_TP_ADD_TC(tp, socket_afinet_bindany);
+ ATF_TP_ADD_TC(tp, socket_afinet_multibind);
+ ATF_TP_ADD_TC(tp, socket_afinet_bind_connected_port);
return atf_no_error();
}
diff --git a/tests/sys/netinet/tcp_implied_connect.c b/tests/sys/netinet/tcp_implied_connect.c
index 6e8cb0606a0a..d03d6be4fb92 100644
--- a/tests/sys/netinet/tcp_implied_connect.c
+++ b/tests/sys/netinet/tcp_implied_connect.c
@@ -51,6 +51,7 @@ ATF_TC_BODY(tcp_implied_connect, tc)
ATF_REQUIRE(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
len = sizeof(sin);
ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &len) == 0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ATF_REQUIRE(listen(s, -1) == 0);
#if 0
/*
diff --git a/tests/sys/netinet/udp_bindings.c b/tests/sys/netinet/udp_bindings.c
new file mode 100644
index 000000000000..b05967d4b080
--- /dev/null
+++ b/tests/sys/netinet/udp_bindings.c
@@ -0,0 +1,249 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 Gleb Smirnoff <glebius@FreeBSD.org>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/jail.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <errno.h>
+#include <ifaddrs.h>
+#include <pthread.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static const char buf[] = "Hello";
+
+static void
+sendtolocalhost(int s)
+{
+ struct sockaddr_in dst = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ .sin_addr = { htonl(INADDR_LOOPBACK) },
+ .sin_port = htons(1638),
+ };
+
+ ATF_REQUIRE(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)&dst,
+ sizeof(dst)) == sizeof(buf));
+}
+
+/*
+ * Echo back to the sender its own address in payload.
+ */
+static void *
+echo(void *arg)
+{
+ int s = *(int *)arg;
+ struct sockaddr_in sin;
+ socklen_t slen = sizeof(sin);
+ char rbuf[sizeof(buf)];
+
+ ATF_REQUIRE(recvfrom(s, &rbuf, sizeof(rbuf), 0, (struct sockaddr *)&sin,
+ &slen) == sizeof(rbuf));
+ printf("Echo to %s:%u\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port));
+ ATF_REQUIRE(sendto(s, &sin, sizeof(sin), 0, (struct sockaddr *)&sin,
+ sizeof(sin)) == sizeof(sin));
+ return (NULL);
+}
+
+/*
+ * Cycle through local addresses (normally there should be at least two
+ * different IPv4 ones), and communicate to the echo server checking both
+ * IP_SENDSRCADDR and IP_RECVDSTADDR. Use same cmsg buffer for both send
+ * and receive operation, this is a suggested in manual, given that
+ * IP_RECVDSTADDR == IP_SENDSRCADDR.
+ * At the setup phase check that IP_SENDSRCADDR doesn't work on unbound socket.
+ */
+ATF_TC_WITHOUT_HEAD(IP_SENDSRCADDR);
+ATF_TC_BODY(IP_SENDSRCADDR, tc)
+{
+ struct sockaddr_in srv = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ }, dst;
+ char cbuf[CMSG_SPACE(sizeof(struct in_addr))];
+ struct iovec iov = {
+ .iov_base = __DECONST(char *, buf),
+ .iov_len = sizeof(buf),
+ };
+ struct iovec riov = {
+ .iov_base = &dst,
+ .iov_len = sizeof(dst),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ .msg_name = &srv,
+ .msg_namelen = sizeof(srv),
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ struct msghdr rmsg = {
+ .msg_iov = &riov,
+ .msg_iovlen = 1,
+ .msg_control = cbuf,
+ .msg_controllen = sizeof(cbuf),
+ };
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ *cmsg = (struct cmsghdr) {
+ .cmsg_level = IPPROTO_IP,
+ .cmsg_type = IP_SENDSRCADDR,
+ .cmsg_len = CMSG_LEN(sizeof(struct in_addr)),
+ };
+ socklen_t slen = sizeof(struct sockaddr_in);
+ struct ifaddrs *ifa0, *ifa;
+ pthread_t tid;
+ int s, e;
+
+ /* First check that IP_SENDSRCADDR doesn't work on an unbound socket. */
+ ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE_MSG(sendmsg(s, &msg, 0) == -1 && errno == EINVAL,
+ "sendmsg(.cmsg_type = IP_SENDSRCADDR), errno %d", errno);
+
+ /* Bind to random ports both sender and echo server. */
+ ATF_REQUIRE(bind(s, (struct sockaddr *)&srv, sizeof(srv)) == 0);
+ ATF_REQUIRE((e = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(bind(e, (struct sockaddr *)&srv, sizeof(srv)) == 0);
+ ATF_REQUIRE(getsockname(e, (struct sockaddr *)&srv, &slen) == 0);
+ srv.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ ATF_REQUIRE(getifaddrs(&ifa0) == 0);
+ for (ifa = ifa0; ifa != NULL; ifa = ifa->ifa_next) {
+ struct sockaddr_in src;
+ struct in_addr vrf;
+
+ if (ifa->ifa_addr->sa_family != AF_INET)
+ continue;
+ memcpy(&src, ifa->ifa_addr, sizeof(src));
+ printf("Sending from %s\n", inet_ntoa(src.sin_addr));
+ ATF_REQUIRE(pthread_create(&tid, NULL, echo, &e) == 0);
+ memcpy(CMSG_DATA(cmsg), &src.sin_addr, sizeof(src.sin_addr));
+ ATF_REQUIRE(sendmsg(s, &msg, 0) == sizeof(buf));
+ ATF_REQUIRE(recvmsg(s, &rmsg, 0) == sizeof(struct sockaddr_in));
+ memcpy(&vrf, CMSG_DATA(cmsg), sizeof(vrf));
+ ATF_REQUIRE_MSG(dst.sin_addr.s_addr == src.sin_addr.s_addr,
+ "Sent from %s, but echo server reports %s",
+ inet_ntoa(src.sin_addr), inet_ntoa(dst.sin_addr));
+ ATF_REQUIRE_MSG(vrf.s_addr == src.sin_addr.s_addr,
+ "Sent from %s, but IP_RECVDSTADDR reports %s",
+ inet_ntoa(src.sin_addr), inet_ntoa(vrf));
+ ATF_REQUIRE(pthread_join(tid, NULL) == 0);
+ }
+
+ freeifaddrs(ifa0);
+ close(s);
+ close(e);
+}
+
+/*
+ * Check gethostname(2) on a newborn socket, and then on an unconnected, but
+ * used socket. The first shall return all-zeroes, and second one should
+ * return us our assigned port.
+ */
+ATF_TC_WITHOUT_HEAD(gethostname);
+ATF_TC_BODY(gethostname, tc)
+{
+ struct sockaddr_in sin;
+ socklen_t slen = sizeof(sin);
+ int s;
+
+ ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0);
+ ATF_REQUIRE_MSG(sin.sin_addr.s_addr == INADDR_ANY && sin.sin_port == 0,
+ "newborn socket name %s:%u", inet_ntoa(sin.sin_addr),
+ ntohs(sin.sin_port));
+ sendtolocalhost(s);
+ ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0);
+ ATF_REQUIRE_MSG(sin.sin_addr.s_addr == INADDR_ANY && sin.sin_port != 0,
+ "used unconnected socket name %s:%u", inet_ntoa(sin.sin_addr),
+ ntohs(sin.sin_port));
+ close(s);
+}
+
+ATF_TC_WITHOUT_HEAD(gethostname_jailed);
+ATF_TC_BODY(gethostname_jailed, tc)
+{
+ struct in_addr laddr = { htonl(INADDR_LOOPBACK) };
+ struct jail jconf = {
+ .version = JAIL_API_VERSION,
+ .path = __DECONST(char *, "/"),
+ .hostname = __DECONST(char *,"test"),
+ .ip4s = 1,
+ .ip4 = &laddr,
+ };
+ struct sockaddr_in sin;
+ socklen_t slen = sizeof(sin);
+ int s;
+
+ ATF_REQUIRE(jail(&jconf) > 0);
+ ATF_REQUIRE((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ sendtolocalhost(s);
+ ATF_REQUIRE(getsockname(s, (struct sockaddr *)&sin, &slen) == 0);
+ ATF_REQUIRE_MSG(sin.sin_addr.s_addr == laddr.s_addr &&
+ sin.sin_port != 0,
+ "jailed unconnected socket name %s:%u", inet_ntoa(sin.sin_addr),
+ ntohs(sin.sin_port));
+ close(s);
+}
+
+/*
+ * See bug 274009.
+ */
+ATF_TC_WITHOUT_HEAD(v4mapped);
+ATF_TC_BODY(v4mapped, tc)
+{
+ struct sockaddr_in6 sa6 = {
+ .sin6_family = AF_INET6,
+ .sin6_len = sizeof(struct sockaddr_in6),
+ .sin6_port = htons(1),
+ };
+ int s;
+
+ ATF_REQUIRE((s = socket(PF_INET6, SOCK_DGRAM, 0)) > 0);
+ ATF_REQUIRE(setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &(int){0},
+ sizeof(int)) == 0);
+ ATF_REQUIRE(inet_pton(AF_INET6, "::ffff:127.0.0.1", &(sa6.sin6_addr))
+ == 1);
+ ATF_REQUIRE(sendto(s, buf, sizeof(buf), 0, (struct sockaddr *)&sa6,
+ sizeof(sa6)) == sizeof(buf));
+ close(s);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, v4mapped);
+ ATF_TP_ADD_TC(tp, gethostname);
+ ATF_TP_ADD_TC(tp, gethostname_jailed);
+ ATF_TP_ADD_TC(tp, IP_SENDSRCADDR);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/udp_io.c b/tests/sys/netinet/udp_io.c
index 27cd02735ed4..04f9bf56ed02 100644
--- a/tests/sys/netinet/udp_io.c
+++ b/tests/sys/netinet/udp_io.c
@@ -52,6 +52,7 @@ udp_socketpair(int *s)
ATF_REQUIRE((c = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
ATF_REQUIRE(bind(b, (struct sockaddr *)&sin, sizeof(sin)) == 0);
ATF_REQUIRE(getsockname(b, (struct sockaddr *)&sin, &slen) == 0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ATF_REQUIRE(connect(c, (struct sockaddr *)&sin, sizeof(sin)) == 0);
s[0] = b;
diff --git a/tests/sys/netinet6/Makefile b/tests/sys/netinet6/Makefile
index 82e84859ecbc..26f1a18a8d32 100644
--- a/tests/sys/netinet6/Makefile
+++ b/tests/sys/netinet6/Makefile
@@ -1,32 +1,52 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netinet6
FILESDIR= ${TESTSDIR}
ATF_TESTS_PYTEST= test_ip6_output.py
-ATF_TESTS_SH= \
- exthdr \
- mld \
- scapyi386 \
- redirect \
- divert \
- forward6 \
- output6 \
- lpm6 \
- fibs6 \
- ndp \
- proxy_ndp
-TEST_METADATA.output6+= required_programs="python"
+ATF_TESTS_SH= exthdr \
+ mld \
+ scapyi386 \
+ redirect \
+ divert \
+ forward6 \
+ output6 \
+ lpm6 \
+ fibs6 \
+ ndp \
+ proxy_ndp \
+ addr6
+
+TEST_METADATA.divert+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.exthdr+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.forward6+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.ndp+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.output6+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets" \
+ required_programs="python"
+TEST_METADATA.proxy_ndp+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.redirect+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.scapyi386+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
+TEST_METADATA.addr6+= execenv="jail" \
+ execenv_jail_params="vnet allow.raw_sockets"
-${PACKAGE}FILES+= exthdr.py
-${PACKAGE}FILES+= mld.py
-${PACKAGE}FILES+= scapyi386.py
-${PACKAGE}FILES+= redirect.py
+${PACKAGE}FILES+= exthdr.py \
+ mld.py \
+ scapyi386.py \
+ ra.py \
+ redirect.py
${PACKAGE}FILESMODE_exthdr.py= 0555
${PACKAGE}FILESMODE_mld.py= 0555
${PACKAGE}FILESMODE_scapyi386.py=0555
+${PACKAGE}FILESMODE_ra.py=0555
${PACKAGE}FILESMODE_redirect.py=0555
TESTS_SUBDIRS+= frag6
diff --git a/tests/sys/netinet6/addr6.sh b/tests/sys/netinet6/addr6.sh
new file mode 100755
index 000000000000..38e4bb152240
--- /dev/null
+++ b/tests/sys/netinet6/addr6.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: ISC
+#
+# Copyright (c) 2025 Lexi Winter.
+#
+# Permission to use, copy, modify, and distribute this software for any
+# purpose with or without fee is hereby granted, provided that the above
+# copyright notice and this permission notice appear in all copies.
+#
+# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+. $(atf_get_srcdir)/../common/vnet.subr
+
+atf_test_case "addr6_invalid_addr" "cleanup"
+addr6_invalid_addr_head()
+{
+ atf_set descr "adding an invalid IPv6 address returns an error"
+ atf_set require.user root
+}
+
+addr6_invalid_addr_body()
+{
+ vnet_init
+
+ ep=$(vnet_mkepair)
+ atf_check -s exit:0 ifconfig ${ep}a inet6 2001:db8::1/128
+ atf_check -s exit:1 -e ignore ifconfig ${ep}a inet6 2001:db8::1/127 alias
+}
+
+addr6_invalid_addr_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "addr6_invalid_addr"
+}
diff --git a/tests/sys/netinet6/divert.sh b/tests/sys/netinet6/divert.sh
index 2cf57c5966b0..e2dc3e26d97e 100755
--- a/tests/sys/netinet6/divert.sh
+++ b/tests/sys/netinet6/divert.sh
@@ -41,11 +41,10 @@ ipdivert_ip6_output_remote_success_head() {
atf_set descr 'Test valid IPv6 redirect'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
ipdivert_ip6_output_remote_success_body() {
-
ids=65530
id=`printf "%x" ${ids}`
if [ $$ -gt 65535 ]; then
diff --git a/tests/sys/netinet6/exthdr.sh b/tests/sys/netinet6/exthdr.sh
index 350307f13eae..3d866d85ea83 100755
--- a/tests/sys/netinet6/exthdr.sh
+++ b/tests/sys/netinet6/exthdr.sh
@@ -32,7 +32,7 @@ exthdr_head() {
atf_set descr 'Test IPv6 extension header code paths'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
exthdr_body() {
diff --git a/tests/sys/netinet6/forward6.sh b/tests/sys/netinet6/forward6.sh
index b3ccd30aea62..e4b027bf281a 100755
--- a/tests/sys/netinet6/forward6.sh
+++ b/tests/sys/netinet6/forward6.sh
@@ -34,7 +34,7 @@ fwd_ip6_gu_icmp_iface_fast_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to interface'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_iface_fast_success_body() {
@@ -104,7 +104,7 @@ fwd_ip6_gu_icmp_gw_gu_fast_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to GU gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_gw_gu_fast_success_body() {
@@ -178,7 +178,7 @@ fwd_ip6_gu_icmp_gw_ll_fast_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to LL gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_gw_ll_fast_success_body() {
@@ -253,7 +253,7 @@ fwd_ip6_gu_icmp_iface_slow_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to interface'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_iface_slow_success_body() {
@@ -322,7 +322,7 @@ fwd_ip6_gu_icmp_gw_gu_slow_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to GU gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_gw_gu_slow_success_body() {
@@ -397,7 +397,7 @@ fwd_ip6_gu_icmp_gw_ll_slow_success_head() {
atf_set descr 'Test valid IPv6 global unicast fast-forwarding to LL gw'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
fwd_ip6_gu_icmp_gw_ll_slow_success_body() {
@@ -466,6 +466,59 @@ fwd_ip6_gu_icmp_gw_ll_slow_success_cleanup() {
vnet_cleanup
}
+atf_test_case "fwd_ip6_blackhole" "cleanup"
+fwd_ip6_blackhole_head() {
+
+ atf_set descr 'Test blackhole routing'
+ atf_set require.user root
+}
+
+fwd_ip6_blackhole_body() {
+ jname="v6t-fwd_ip6_blackhole"
+
+ vnet_init
+
+ epair=$(vnet_mkepair)
+ epair_out=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+
+ vnet_mkjail ${jname} ${epair}b ${epair_out}b
+ jexec ${jname} ifconfig lo0 inet6 ::1/128 up no_dad
+ jexec ${jname} ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec ${jname} ifconfig ${epair_out}b inet6 2001:db8:1::1/64 up no_dad
+ jexec ${jname} sysctl net.inet6.ip6.forwarding=1
+
+ route -6 add default 2001:db8::1
+
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 1 -t 1 2001:db8:1::2
+ atf_check -s exit:0 -o match:"0 packets not forwardable" \
+ jexec ${jname} netstat -s -p ip6
+
+ # Create blackhole route
+ jexec ${jname} route -6 add 2001:db8:1::2 -blackhole
+
+ # Force slow path
+ jexec ${jname} sysctl net.inet6.ip6.redirect=1
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 1 -t 1 2001:db8:1::2
+ atf_check -s exit:0 -o match:"1 packet not forwardable" \
+ jexec ${jname} netstat -s -p ip6
+
+ # Now try the fast path
+ jexec ${jname} sysctl net.inet6.ip6.redirect=0
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 1 -t 1 2001:db8:1::2
+ atf_check -s exit:0 -o match:"2 packets not forwardable" \
+ jexec ${jname} netstat -s -p ip6
+}
+
+fwd_ip6_blackhole_cleanup() {
+
+ vnet_cleanup
+}
+
atf_init_test_cases()
{
@@ -475,6 +528,7 @@ atf_init_test_cases()
atf_add_test_case "fwd_ip6_gu_icmp_iface_slow_success"
atf_add_test_case "fwd_ip6_gu_icmp_gw_gu_slow_success"
atf_add_test_case "fwd_ip6_gu_icmp_gw_ll_slow_success"
+ atf_add_test_case "fwd_ip6_blackhole"
}
# end
diff --git a/tests/sys/netinet6/frag6/Makefile b/tests/sys/netinet6/frag6/Makefile
index d1661060368b..3fca0522e533 100644
--- a/tests/sys/netinet6/frag6/Makefile
+++ b/tests/sys/netinet6/frag6/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netinet6/frag6
diff --git a/tests/sys/netinet6/frag6/frag6.subr b/tests/sys/netinet6/frag6/frag6.subr
index 70e7386b60cc..238c9619c398 100644
--- a/tests/sys/netinet6/frag6/frag6.subr
+++ b/tests/sys/netinet6/frag6/frag6.subr
@@ -31,7 +31,7 @@ frag6_head()
{
atf_set descr 'Test IPv6 fragmentation code'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
frag6_body()
diff --git a/tests/sys/netinet6/mld.sh b/tests/sys/netinet6/mld.sh
index 7229aa34dc52..d98624daedf5 100755
--- a/tests/sys/netinet6/mld.sh
+++ b/tests/sys/netinet6/mld.sh
@@ -32,7 +32,7 @@ mldraw01_head() {
atf_set descr 'Test for correct Ethernet Destination MAC address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
mldraw01_body() {
diff --git a/tests/sys/netinet6/ndp.sh b/tests/sys/netinet6/ndp.sh
index eddd49112421..bac9764ee3c9 100755
--- a/tests/sys/netinet6/ndp.sh
+++ b/tests/sys/netinet6/ndp.sh
@@ -25,7 +25,6 @@
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
# SUCH DAMAGE.
#
-#
. $(atf_get_srcdir)/../common/vnet.subr
@@ -36,6 +35,7 @@ ndp_add_gu_success_head() {
}
ndp_add_gu_success_body() {
+ local epair0 jname
vnet_init
@@ -74,6 +74,7 @@ ndp_del_gu_success_head() {
}
ndp_del_gu_success_body() {
+ local epair0 jname
vnet_init
@@ -102,13 +103,94 @@ ndp_del_gu_success_cleanup() {
vnet_cleanup
}
+ndp_if_up()
+{
+ local ifname=$1
+ local jname=$2
-atf_init_test_cases()
+ if [ -n "$jname" ]; then
+ jname="jexec ${jname}"
+ fi
+ atf_check ${jname} ifconfig ${ifname} up
+ atf_check ${jname} ifconfig ${ifname} inet6 -ifdisabled
+ while ${jname} ifconfig ${ifname} inet6 | grep tentative; do
+ sleep 0.1
+ done
+}
+
+ndp_if_lladdr()
{
+ local ifname=$1
+ local jname=$2
- atf_add_test_case "ndp_add_gu_success"
- atf_add_test_case "ndp_del_gu_success"
+ if [ -n "$jname" ]; then
+ jname="jexec ${jname}"
+ fi
+ ${jname} ifconfig ${ifname} inet6 | \
+ awk '/inet6 fe80:/{split($2, addr, "%"); print addr[1]}'
}
-# end
+atf_test_case "ndp_slaac_default_route" "cleanup"
+ndp_slaac_default_route_head() {
+ atf_set descr 'Test default route installation via SLAAC'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+ndp_slaac_default_route_body() {
+ local epair0 jname lladdr
+
+ vnet_init
+
+ jname="v6t-ndp_slaac_default_route"
+
+ epair0=$(vnet_mkepair)
+ vnet_mkjail ${jname} ${epair0}a
+
+ ndp_if_up ${epair0}a ${jname}
+ ndp_if_up ${epair0}b
+ atf_check jexec ${jname} ifconfig ${epair0}a inet6 accept_rtadv
+
+ # Send an RA advertising a prefix.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --prefix "2001:db8:ffff:1000::" --prefixlen 64
+
+ # Wait for a default router to appear.
+ while [ -z "$(jexec ${jname} ndp -r)" ]; do
+ sleep 0.1
+ done
+ atf_check -o match:"^default[[:space:]]+fe80:" \
+ jexec ${jname} netstat -rn -6
+
+ # Get rid of the default route.
+ jexec ${jname} route -6 flush
+ atf_check -o not-match:"^default[[:space:]]+fe80:" \
+ jexec ${jname} netstat -rn -6
+
+ # Send another RA, make sure that the default route is installed again.
+ atf_check -e ignore python3 $(atf_get_srcdir)/ra.py \
+ --sendif ${epair0}b \
+ --dst $(ndp_if_lladdr ${epair0}a ${jname}) \
+ --src $(ndp_if_lladdr ${epair0}b) \
+ --prefix "2001:db8:ffff:1000::" --prefixlen 64
+ while [ -z "$(jexec ${jname} ndp -r)" ]; do
+ sleep 0.1
+ done
+ atf_check -o match:"^default[[:space:]]+fe80:" \
+ jexec ${jname} netstat -rn -6
+}
+
+ndp_slaac_default_route_cleanup() {
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "ndp_add_gu_success"
+ atf_add_test_case "ndp_del_gu_success"
+ atf_add_test_case "ndp_slaac_default_route"
+}
diff --git a/tests/sys/netinet6/ra.py b/tests/sys/netinet6/ra.py
new file mode 100644
index 000000000000..44814418da48
--- /dev/null
+++ b/tests/sys/netinet6/ra.py
@@ -0,0 +1,38 @@
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Klara, Inc.
+#
+
+import argparse
+import scapy.all as sp
+import sys
+
+#
+# Emit a router advertisement with the specified prefix.
+#
+def main():
+ parser = argparse.ArgumentParser("ra.py",
+ description="Emits Router Advertisement packets")
+ parser.add_argument('--sendif', nargs=1, required=True,
+ help='The interface through which the packet will be sent')
+ parser.add_argument('--src', nargs=1, required=True,
+ help='The source IP address')
+ parser.add_argument('--dst', nargs=1, required=True,
+ help='The destination IP address')
+ parser.add_argument('--prefix', nargs=1, required=True,
+ help='The prefix to be advertised')
+ parser.add_argument('--prefixlen', nargs=1, required=True, type=int,
+ help='The prefix length to be advertised')
+
+ args = parser.parse_args()
+ pkt = sp.Ether() / \
+ sp.IPv6(src=args.src, dst=args.dst) / \
+ sp.ICMPv6ND_RA(chlim=64) / \
+ sp.ICMPv6NDOptPrefixInfo(prefix=args.prefix, prefixlen=args.prefixlen)
+
+ sp.sendp(pkt, iface=args.sendif[0], verbose=False)
+ sys.exit(0)
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/sys/netinet6/redirect.sh b/tests/sys/netinet6/redirect.sh
index 64efe7339ffd..40874f8c9b6d 100644
--- a/tests/sys/netinet6/redirect.sh
+++ b/tests/sys/netinet6/redirect.sh
@@ -34,15 +34,11 @@ valid_redirect_head() {
atf_set descr 'Test valid IPv6 redirect'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
valid_redirect_body() {
- if [ "$(atf_config_get ci false)" = "true" ]; then
- atf_skip "https://bugs.freebsd.org/247729"
- fi
-
ids=65533
id=`printf "%x" ${ids}`
if [ $$ -gt 65535 ]; then
@@ -89,7 +85,7 @@ valid_redirect_body() {
while [ `ifconfig ${epair}a inet6 | grep -c tentative` != "0" ]; do
sleep 0.1
done
- while [ `jexec ${jname}b ifconfig ${epair}b inet6 | grep -c tentative` != "0" ]; do
+ while [ `jexec ${jname} ifconfig ${epair}b inet6 | grep -c tentative` != "0" ]; do
sleep 0.1
done
diff --git a/tests/sys/netinet6/scapyi386.sh b/tests/sys/netinet6/scapyi386.sh
index bb9ee4cfcd2a..2d91f25dd01e 100755
--- a/tests/sys/netinet6/scapyi386.sh
+++ b/tests/sys/netinet6/scapyi386.sh
@@ -32,7 +32,7 @@ scapyi386_head() {
atf_set descr 'Test for correct Ethernet Destination MAC address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
scapyi386_body() {
diff --git a/tests/sys/netipsec/Makefile b/tests/sys/netipsec/Makefile
index af5c0f75e8ba..bb10297e0cc3 100644
--- a/tests/sys/netipsec/Makefile
+++ b/tests/sys/netipsec/Makefile
@@ -1,4 +1,3 @@
-
TESTSDIR= ${TESTSBASE}/sys/netipsec
TESTS_SUBDIRS+= tunnel
diff --git a/tests/sys/netipsec/tunnel/Makefile b/tests/sys/netipsec/tunnel/Makefile
index c28826711cb8..c6060a790cc3 100644
--- a/tests/sys/netipsec/tunnel/Makefile
+++ b/tests/sys/netipsec/tunnel/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netipsec/tunnel
diff --git a/tests/sys/netipsec/tunnel/empty.sh b/tests/sys/netipsec/tunnel/empty.sh
index dc1d3708f744..56480d21f4ec 100755
--- a/tests/sys/netipsec/tunnel/empty.sh
+++ b/tests/sys/netipsec/tunnel/empty.sh
@@ -11,7 +11,7 @@ v4_head()
v4_body()
{
# Can't use filename "null" for this script: PR 223564
- ist_test 4 null ""
+ ist_test 4 null "1234"
}
v4_cleanup()
@@ -28,7 +28,7 @@ v6_head()
v6_body()
{
- ist_test 6 null ""
+ ist_test 6 null "5678"
}
v6_cleanup()
diff --git a/tests/sys/netlink/Makefile b/tests/sys/netlink/Makefile
index c52149674ceb..c07ef8663867 100644
--- a/tests/sys/netlink/Makefile
+++ b/tests/sys/netlink/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
WARNS?= 1
diff --git a/tests/sys/netlink/test_rtnl_ifaddr.py b/tests/sys/netlink/test_rtnl_ifaddr.py
index 48e53502df54..768bf38153ff 100644
--- a/tests/sys/netlink/test_rtnl_ifaddr.py
+++ b/tests/sys/netlink/test_rtnl_ifaddr.py
@@ -33,7 +33,10 @@ class TestRtNlIfaddrList(NetlinkTestTemplate, SingleVnetTestTemplate):
def setup_method(self, method):
method_name = method.__name__
if "4" in method_name:
- self.IPV4_PREFIXES = ["192.0.2.1/24"]
+ if "nofilter" in method_name:
+ self.IPV4_PREFIXES = ["192.0.2.1/24", "169.254.169.254/16"]
+ else:
+ self.IPV4_PREFIXES = ["192.0.2.1/24"]
if "6" in method_name:
self.IPV6_PREFIXES = ["2001:db8::1/64"]
super().setup_method(method)
@@ -49,14 +52,21 @@ class TestRtNlIfaddrList(NetlinkTestTemplate, SingleVnetTestTemplate):
for rx_msg in self.read_msg_list(msg.nl_hdr.nlmsg_seq, NlRtMsgType.RTM_NEWADDR):
ifname = socket.if_indextoname(rx_msg.base_hdr.ifa_index)
family = rx_msg.base_hdr.ifa_family
- ret.append((ifname, family, rx_msg))
+ scope = rx_msg.base_hdr.ifa_scope
+ ret.append((ifname, family, scope))
ifname = "lo0"
- assert len([r for r in ret if r[0] == ifname]) > 0
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_HOST.value]) == 1
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_HOST.value]) == 1
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1
+ assert len([r for r in ret if r[0] == ifname]) == 3
ifname = self.vnet.iface_alias_map["if1"].name
- assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET]) == 1
- assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6]) == 2
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET and r[2] == RtScope.RT_SCOPE_UNIVERSE.value]) == 1
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_LINK.value]) == 1
+ assert len([r for r in ret if r[0] == ifname and r[1] == socket.AF_INET6 and r[2] == RtScope.RT_SCOPE_UNIVERSE.value]) == 1
+ assert len([r for r in ret if r[0] == ifname]) == 4
def test_46_filter_iface(self):
"""Tests that listing outputs both IPv4/IPv6 for the specific interface"""
diff --git a/tests/sys/netlink/test_snl.c b/tests/sys/netlink/test_snl.c
index bd607efa82fc..040414a96e2c 100644
--- a/tests/sys/netlink/test_snl.c
+++ b/tests/sys/netlink/test_snl.c
@@ -13,6 +13,18 @@
#include <atf-c.h>
+static const struct snl_hdr_parser *snl_all_core_parsers[] = {
+ &snl_errmsg_parser, &snl_donemsg_parser,
+ &_nla_bit_parser, &_nla_bitset_parser,
+};
+
+static const struct snl_hdr_parser *snl_all_route_parsers[] = {
+ &_metrics_mp_nh_parser, &_mpath_nh_parser, &_metrics_parser, &snl_rtm_route_parser,
+ &_link_fbsd_parser, &snl_rtm_link_parser, &snl_rtm_link_parser_simple,
+ &_neigh_fbsd_parser, &snl_rtm_neigh_parser,
+ &_addr_fbsd_parser, &snl_rtm_addr_parser, &_nh_fbsd_parser, &snl_nhmsg_parser,
+};
+
static void
require_netlink(void)
{
diff --git a/tests/sys/netlink/test_snl_generic.c b/tests/sys/netlink/test_snl_generic.c
index d84d3f88f487..c63b1380f2ad 100644
--- a/tests/sys/netlink/test_snl_generic.c
+++ b/tests/sys/netlink/test_snl_generic.c
@@ -11,6 +11,10 @@
#include <atf-c.h>
+static const struct snl_hdr_parser *snl_all_genl_parsers[] = {
+ &_genl_ctrl_getfam_parser, &_genl_ctrl_mc_parser,
+};
+
static void
require_netlink(void)
{
@@ -98,7 +102,7 @@ ATF_TC_BODY(test_snl_get_genl_family_groups, tc)
ATF_CHECK(snl_parse_nlmsg(&ss, hdr, &_genl_ctrl_getfam_parser, &attrs));
ATF_CHECK_EQ(attrs.mcast_groups.num_groups, 1);
- struct snl_genl_ctrl_mcast_group *group = attrs.mcast_groups.groups[0];
+ struct _snl_genl_ctrl_mcast_group *group = attrs.mcast_groups.groups[0];
ATF_CHECK(group->mcast_grp_id > 0);
ATF_CHECK(!strcmp(group->mcast_grp_name, "notify"));
diff --git a/tests/sys/netmap/Makefile b/tests/sys/netmap/Makefile
index 8c3ae26284ec..ee00d0421620 100644
--- a/tests/sys/netmap/Makefile
+++ b/tests/sys/netmap/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netmap
diff --git a/tests/sys/netmap/ctrl-api-test.c b/tests/sys/netmap/ctrl-api-test.c
index 8d33b4c58d2a..6b45dbb1cfea 100644
--- a/tests/sys/netmap/ctrl-api-test.c
+++ b/tests/sys/netmap/ctrl-api-test.c
@@ -1596,6 +1596,7 @@ sync_kloop_csb_enable(struct TestContext *ctx)
return sync_kloop_start_stop(ctx);
}
+#if 0
static int
sync_kloop_conflict(struct TestContext *ctx)
{
@@ -1640,6 +1641,14 @@ sync_kloop_conflict(struct TestContext *ctx)
/* Wait for one of the two threads to fail to start the kloop, to
* avoid a race condition where th1 starts the loop and stops,
* and after that th2 starts the loop successfully. */
+ /*
+ * XXX: This doesn't fully close the race. th2 might fail to
+ * start executing since th1 can enter the kernel and hog the
+ * CPU on a single-CPU system until the semaphore timeout
+ * awakens this thread and it calls sync_kloop_stop. Once th1
+ * exits the kernel, th2 can finally run and will then loop
+ * forever in the ioctl handler.
+ */
clock_gettime(CLOCK_REALTIME, &to);
to.tv_sec += 2;
ret = sem_timedwait(&sem, &to);
@@ -1674,6 +1683,7 @@ sync_kloop_conflict(struct TestContext *ctx)
? 0
: -1;
}
+#endif
static int
sync_kloop_eventfds_mismatch(struct TestContext *ctx)
@@ -2079,7 +2089,9 @@ static struct mytest tests[] = {
decltest(sync_kloop_eventfds_all_direct_rx),
decltest(sync_kloop_nocsb),
decltest(sync_kloop_csb_enable),
+#if 0
decltest(sync_kloop_conflict),
+#endif
decltest(sync_kloop_eventfds_mismatch),
decltest(null_port),
decltest(null_port_all_zero),
diff --git a/tests/sys/netpfil/Makefile b/tests/sys/netpfil/Makefile
index bdbb9078cfc2..b449902aabc2 100644
--- a/tests/sys/netpfil/Makefile
+++ b/tests/sys/netpfil/Makefile
@@ -1,4 +1,3 @@
-
.include <src.opts.mk>
TESTSDIR= ${TESTSBASE}/sys/netpfil
diff --git a/tests/sys/netpfil/common/Makefile b/tests/sys/netpfil/common/Makefile
index 0003aac28779..0938bd9d9c7e 100644
--- a/tests/sys/netpfil/common/Makefile
+++ b/tests/sys/netpfil/common/Makefile
@@ -1,7 +1,7 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netpfil/common
+BINDIR= ${TESTSDIR}
ATF_TESTS_SH+= \
@@ -13,8 +13,11 @@ ATF_TESTS_SH+= \
fragments \
forward
-# Tests reuse jail names and so cannot run in parallel.
-TEST_METADATA+= is_exclusive=true
+# Allow tests to run in parallel in their own jails
+TEST_METADATA+= execenv="jail"
+TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets"
+
+PROGS= divapp
${PACKAGE}FILES+= \
utils.subr \
diff --git a/tests/sys/netpfil/pf/divapp.c b/tests/sys/netpfil/common/divapp.c
index 908c41eaa67f..d0f4b345b14c 100644
--- a/tests/sys/netpfil/pf/divapp.c
+++ b/tests/sys/netpfil/common/divapp.c
@@ -25,7 +25,7 @@
* SUCH DAMAGE.
*/
-/* Used by tests like divert-to.sh */
+/* Used by divert(4) related tests */
#include <errno.h>
#include <stdlib.h>
@@ -83,8 +83,8 @@ recv_pkt(struct context *c)
s = select(c->fd + 1, &readfds, 0, 0, &timeout);
if (s == -1)
errx(EX_IOERR, "recv_pkt: select() errors.");
- if (s != 1) // timeout
- return -1;
+ if (s != 1) /* timeout */
+ return (-1);
c->pkt_n = recvfrom(c->fd, c->pkt, sizeof(c->pkt), 0,
(struct sockaddr *) &c->sin, &c->sin_len);
@@ -98,14 +98,11 @@ static void
send_pkt(struct context *c)
{
ssize_t n;
- char errstr[32];
n = sendto(c->fd, c->pkt, c->pkt_n, 0,
(struct sockaddr *) &c->sin, c->sin_len);
- if (n == -1) {
- strerror_r(errno, errstr, sizeof(errstr));
- errx(EX_IOERR, "send_pkt: sendto() errors: %d %s.", errno, errstr);
- }
+ if (n == -1)
+ err(EX_IOERR, "send_pkt: sendto() errors");
if (n != c->pkt_n)
errx(EX_IOERR, "send_pkt: sendto() sent %zd of %zd bytes.",
n, c->pkt_n);
@@ -145,5 +142,5 @@ main(int argc, char *argv[])
if (npkt != 1)
errx(EXIT_FAILURE, "%d: npkt=%d.", c.divert_port, npkt);
- return EXIT_SUCCESS;
+ return (EXIT_SUCCESS);
}
diff --git a/tests/sys/netpfil/common/dummynet.sh b/tests/sys/netpfil/common/dummynet.sh
index 3c930cfe2aff..b77b2df84010 100644
--- a/tests/sys/netpfil/common/dummynet.sh
+++ b/tests/sys/netpfil/common/dummynet.sh
@@ -277,7 +277,7 @@ queue_body()
ifconfig ${epair}a 192.0.2.1/24 up
jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/../pf/echo_inetd.conf
# Sanity check
@@ -320,7 +320,7 @@ queue_body()
# TCP should still just pass
fails=0
- for i in `seq 1 3`
+ for i in `seq 1 5`
do
result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c)
if [ $result -ne 2048000 ];
@@ -329,7 +329,7 @@ queue_body()
fails=$(( ${fails} + 1 ))
fi
done
- if [ ${fails} -gt 0 ];
+ if [ ${fails} -gt 2 ];
then
atf_fail "We failed prioritisation ${fails} times"
fi
@@ -348,7 +348,7 @@ queue_body()
sleep 1
fails=0
- for i in `seq 1 3`
+ for i in `seq 1 5`
do
result=$(dd if=/dev/zero bs=1024 count=2000 | timeout 3 nc -w 5 -N 192.0.2.2 7 | wc -c)
if [ $result -ne 2048000 ];
@@ -385,7 +385,7 @@ queue_v6_body()
ifconfig ${epair}a inet6 2001:db8:42::1/64 no_dad up
jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2 no_dad up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/../pf/echo_inetd.conf
jexec alcatraz sysctl net.inet6.icmp6.errppslimit=0
@@ -429,7 +429,7 @@ queue_v6_body()
# TCP should still just pass
fails=0
- for i in `seq 1 3`
+ for i in `seq 1 5`
do
result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c)
if [ $result -ne 1024000 ];
@@ -438,7 +438,7 @@ queue_v6_body()
fails=$(( ${fails} + 1 ))
fi
done
- if [ ${fails} -gt 0 ];
+ if [ ${fails} -gt 2 ];
then
atf_fail "We failed prioritisation ${fails} times"
fi
@@ -454,7 +454,7 @@ queue_v6_body()
"pass in proto icmp6 dnqueue (0, 100)"
fails=0
- for i in `seq 1 3`
+ for i in `seq 1 5`
do
result=$(dd if=/dev/zero bs=1024 count=1000 | timeout 3 nc -w 5 -N 2001:db8:42::2 7 | wc -c)
if [ $result -ne 1024000 ];
@@ -557,7 +557,7 @@ pls_basic_body()
# are dropped (84 - 96 responses).
# repeat up to 6 times if the initial
# checks fail
- atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2
+ atf_check -s exit:0 -o match:'100 packets transmitted, (8[4-9]|9[0-6]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2
}
pls_basic_cleanup()
@@ -604,7 +604,7 @@ pls_gilbert_body()
# are dropped (70 - 85 responses).
# repeat up to 6 times if the initial
# checks fail
- atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 6:10 ping -i 0.010 -c 100 192.0.2.2
+ atf_check -s exit:0 -o match:'100 packets transmitted, (7[0-9]|8[0-5]) packets received' -r 20:10 ping -i 0.010 -c 100 192.0.2.2
}
pls_gilbert_cleanup()
diff --git a/tests/sys/netpfil/common/forward.sh b/tests/sys/netpfil/common/forward.sh
index 939ce55f2d33..fa1f97aa0390 100644
--- a/tests/sys/netpfil/common/forward.sh
+++ b/tests/sys/netpfil/common/forward.sh
@@ -33,7 +33,7 @@ v4_head()
{
atf_set descr 'Basic forwarding test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v4_body()
diff --git a/tests/sys/netpfil/common/pft_ping.py b/tests/sys/netpfil/common/pft_ping.py
index a24a1e00150a..a2a1d9c7f4ec 100644
--- a/tests/sys/netpfil/common/pft_ping.py
+++ b/tests/sys/netpfil/common/pft_ping.py
@@ -33,6 +33,7 @@ logging.getLogger("scapy").setLevel(logging.CRITICAL)
import math
import scapy.all as sp
import sys
+import socket
from copy import copy
from sniffer import Sniffer
@@ -49,8 +50,17 @@ def build_payload(l):
return ret
-def prepare_ipv6(dst_address, send_params):
+def clean_params(params):
+ # Prepare a copy of safe copy of params
+ ret = copy(params)
+ ret.pop('src_address')
+ ret.pop('dst_address')
+ ret.pop('flags')
+ return ret
+
+def prepare_ipv6(send_params):
src_address = send_params.get('src_address')
+ dst_address = send_params.get('dst_address')
hlim = send_params.get('hlim')
tc = send_params.get('tc')
ip6 = sp.IPv6(dst=dst_address)
@@ -63,8 +73,9 @@ def prepare_ipv6(dst_address, send_params):
return ip6
-def prepare_ipv4(dst_address, send_params):
+def prepare_ipv4(send_params):
src_address = send_params.get('src_address')
+ dst_address = send_params.get('dst_address')
flags = send_params.get('flags')
tos = send_params.get('tc')
ttl = send_params.get('hlim')
@@ -84,22 +95,22 @@ def prepare_ipv4(dst_address, send_params):
return ip
-def send_icmp_ping(dst_address, sendif, send_params):
+def send_icmp_ping(send_params):
send_length = send_params['length']
send_frag_length = send_params['frag_length']
packets = []
ether = sp.Ether()
- if ':' in dst_address:
- ip6 = prepare_ipv6(dst_address, send_params)
+ if ':' in send_params['dst_address']:
+ ip6 = prepare_ipv6(send_params)
icmp = sp.ICMPv6EchoRequest(data=sp.raw(build_payload(send_length)))
if send_frag_length:
- for packet in sp.fragment(ip6 / icmp, fragsize=send_frag_length):
+ for packet in sp.fragment6(ip6 / icmp, fragSize=send_frag_length):
packets.append(ether / packet)
else:
packets.append(ether / ip6 / icmp)
else:
- ip = prepare_ipv4(dst_address, send_params)
+ ip = prepare_ipv4(send_params)
icmp = sp.ICMP(type='echo-request')
raw = sp.raw(build_payload(send_length))
if send_frag_length:
@@ -108,10 +119,10 @@ def send_icmp_ping(dst_address, sendif, send_params):
else:
packets.append(ether / ip / icmp / raw)
for packet in packets:
- sp.sendp(packet, sendif, verbose=False)
+ sp.sendp(packet, iface=send_params['sendif'], verbose=False)
-def send_tcp_syn(dst_address, sendif, send_params):
+def send_tcp_syn(send_params):
tcpopt_unaligned = send_params.get('tcpopt_unaligned')
seq = send_params.get('seq')
mss = send_params.get('mss')
@@ -119,22 +130,63 @@ def send_tcp_syn(dst_address, sendif, send_params):
opts=[('Timestamp', (1, 1)), ('MSS', mss if mss else 1280)]
if tcpopt_unaligned:
opts = [('NOP', 0 )] + opts
- if ':' in dst_address:
- ip = prepare_ipv6(dst_address, send_params)
+ if ':' in send_params['dst_address']:
+ ip = prepare_ipv6(send_params)
else:
- ip = prepare_ipv4(dst_address, send_params)
- tcp = sp.TCP(dport=666, flags='S', options=opts, seq=seq)
+ ip = prepare_ipv4(send_params)
+ tcp = sp.TCP(
+ sport=send_params.get('sport'), dport=send_params.get('dport'),
+ flags='S', options=opts, seq=seq,
+ )
req = ether / ip / tcp
- sp.sendp(req, iface=sendif, verbose=False)
+ sp.sendp(req, iface=send_params['sendif'], verbose=False)
+
+
+def send_udp(send_params):
+ LOGGER.debug(f'Sending UDP ping')
+ packets = []
+ send_length = send_params['length']
+ send_frag_length = send_params['frag_length']
+ ether = sp.Ether()
+ if ':' in send_params['dst_address']:
+ ip6 = prepare_ipv6(send_params)
+ udp = sp.UDP(
+ sport=send_params.get('sport'), dport=send_params.get('dport'),
+ )
+ raw = sp.Raw(load=build_payload(send_length))
+ if send_frag_length:
+ for packet in sp.fragment6(ip6 / udp / raw, fragSize=send_frag_length):
+ packets.append(ether / packet)
+ else:
+ packets.append(ether / ip6 / udp / raw)
+ else:
+ ip = prepare_ipv4(send_params)
+ udp = sp.UDP(
+ sport=send_params.get('sport'), dport=send_params.get('dport'),
+ )
+ raw = sp.Raw(load=build_payload(send_length))
+ if send_frag_length:
+ for packet in sp.fragment(ip / udp / raw, fragsize=send_frag_length):
+ packets.append(ether / packet)
+ else:
+ packets.append(ether / ip / udp / raw)
+
+ for packet in packets:
+ sp.sendp(packet, iface=send_params['sendif'], verbose=False)
-def send_ping(dst_address, sendif, ping_type, send_params):
+def send_ping(ping_type, send_params):
if ping_type == 'icmp':
- send_icmp_ping(dst_address, sendif, send_params)
- elif ping_type == 'tcpsyn':
- send_tcp_syn(dst_address, sendif, send_params)
+ send_icmp_ping(send_params)
+ elif (
+ ping_type == 'tcpsyn' or
+ ping_type == 'tcp3way'
+ ):
+ send_tcp_syn(send_params)
+ elif ping_type == 'udp':
+ send_udp(send_params)
else:
- raise Exception('Unspported ping type')
+ raise Exception('Unsupported ping type')
def check_ipv4(expect_params, packet):
@@ -144,20 +196,15 @@ def check_ipv4(expect_params, packet):
tos = expect_params.get('tc')
ttl = expect_params.get('hlim')
ip = packet.getlayer(sp.IP)
+ LOGGER.debug(f'Packet: {ip}')
if not ip:
LOGGER.debug('Packet is not IPv4!')
return False
if src_address and ip.src != src_address:
- LOGGER.debug('Source IPv4 address does not match!')
+ LOGGER.debug(f'Wrong IPv4 source {ip.src}, expected {src_address}')
return False
if dst_address and ip.dst != dst_address:
- LOGGER.debug('Destination IPv4 address does not match!')
- return False
- chksum = ip.chksum
- ip.chksum = None
- new_chksum = sp.IP(sp.raw(ip)).chksum
- if chksum != new_chksum:
- LOGGER.debug(f'Expected IP checksum {new_chksum} but found {chksum}')
+ LOGGER.debug(f'Wrong IPv4 destination {ip.dst}, expected {dst_address}')
return False
if flags and ip.flags != flags:
LOGGER.debug(f'Wrong IP flags value {ip.flags}, expected {flags}')
@@ -181,11 +228,13 @@ def check_ipv6(expect_params, packet):
if not ip6:
LOGGER.debug('Packet is not IPv6!')
return False
- if src_address and ip6.src != src_address:
- LOGGER.debug('Source IPv6 address does not match!')
+ if src_address and socket.inet_pton(socket.AF_INET6, ip6.src) != \
+ socket.inet_pton(socket.AF_INET6, src_address):
+ LOGGER.debug(f'Wrong IPv6 source {ip6.src}, expected {src_address}')
return False
- if dst_address and ip6.dst != dst_address:
- LOGGER.debug('Destination IPv6 address does not match!')
+ if dst_address and socket.inet_pton(socket.AF_INET6, ip6.dst) != \
+ socket.inet_pton(socket.AF_INET6, dst_address):
+ LOGGER.debug(f'Wrong IPv6 destination {ip6.dst}, expected {dst_address}')
return False
# IPv6 has no IP-level checksum.
if flags:
@@ -265,32 +314,32 @@ def check_ping_reply_6(expect_params, packet):
return True
-def check_ping_request(expect_params, packet):
- src_address = expect_params.get('src_address')
- dst_address = expect_params.get('dst_address')
+def check_ping_request(args, packet):
+ src_address = args['expect_params'].get('src_address')
+ dst_address = args['expect_params'].get('dst_address')
if not (src_address or dst_address):
raise Exception('Source or destination address must be given to match the ping request!')
if (
(src_address and ':' in src_address) or
(dst_address and ':' in dst_address)
):
- return check_ping_request_6(expect_params, packet)
+ return check_ping_request_6(args['expect_params'], packet)
else:
- return check_ping_request_4(expect_params, packet)
+ return check_ping_request_4(args['expect_params'], packet)
-def check_ping_reply(expect_params, packet):
- src_address = expect_params.get('src_address')
- dst_address = expect_params.get('dst_address')
+def check_ping_reply(args, packet):
+ src_address = args['expect_params'].get('src_address')
+ dst_address = args['expect_params'].get('dst_address')
if not (src_address or dst_address):
raise Exception('Source or destination address must be given to match the ping reply!')
if (
(src_address and ':' in src_address) or
(dst_address and ':' in dst_address)
):
- return check_ping_reply_6(expect_params, packet)
+ return check_ping_reply_6(args['expect_params'], packet)
else:
- return check_ping_reply_4(expect_params, packet)
+ return check_ping_reply_4(args['expect_params'], packet)
def check_tcp(expect_params, packet):
@@ -305,7 +354,7 @@ def check_tcp(expect_params, packet):
tcp.chksum = None
newpacket = sp.Ether(sp.raw(packet[sp.Ether]))
new_chksum = newpacket[sp.TCP].chksum
- if chksum != new_chksum:
+ if new_chksum and chksum != new_chksum:
LOGGER.debug(f'Wrong TCP checksum {chksum}, expected {new_chksum}!')
return False
if tcp_flags and tcp.flags != tcp_flags:
@@ -328,6 +377,30 @@ def check_tcp(expect_params, packet):
return True
+def check_udp(expect_params, packet):
+ expect_length = expect_params['length']
+ udp = packet.getlayer(sp.UDP)
+ if not udp:
+ LOGGER.debug('Packet is not UDP!')
+ return False
+ raw = packet.getlayer(sp.Raw)
+ if not raw:
+ LOGGER.debug('Packet contains no payload!')
+ return False
+ if raw.load != build_payload(expect_length):
+ LOGGER.debug(f'Payload magic does not match len {len(raw.load)} vs {expect_length}!')
+ return False
+ orig_chksum = udp.chksum
+ udp.chksum = None
+ newpacket = sp.Ether(sp.raw(packet[sp.Ether]))
+ new_chksum = newpacket[sp.UDP].chksum
+ if new_chksum and orig_chksum != new_chksum:
+ LOGGER.debug(f'Wrong UDP checksum {orig_chksum}, expected {new_chksum}!')
+ return False
+
+ return True
+
+
def check_tcp_syn_request_4(expect_params, packet):
if not check_ipv4(expect_params, packet):
return False
@@ -336,7 +409,7 @@ def check_tcp_syn_request_4(expect_params, packet):
return True
-def check_tcp_syn_reply_4(expect_params, packet):
+def check_tcp_syn_reply_4(send_params, expect_params, packet):
if not check_ipv4(expect_params, packet):
return False
if not check_tcp(expect_params | {'tcp_flags': 'SA'}, packet):
@@ -344,6 +417,44 @@ def check_tcp_syn_reply_4(expect_params, packet):
return True
+def check_tcp_3way_4(args, packet):
+ send_params = args['send_params']
+
+ expect_params_sa = clean_params(args['expect_params'])
+ expect_params_sa['src_address'] = send_params['dst_address']
+ expect_params_sa['dst_address'] = send_params['src_address']
+
+ # Sniff incoming SYN+ACK packet
+ if (
+ check_ipv4(expect_params_sa, packet) and
+ check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
+ ):
+ ether = sp.Ether()
+ ip_sa = packet.getlayer(sp.IP)
+ tcp_sa = packet.getlayer(sp.TCP)
+ reply_params = clean_params(send_params)
+ reply_params['src_address'] = ip_sa.dst
+ reply_params['dst_address'] = ip_sa.src
+ ip_a = prepare_ipv4(reply_params)
+ tcp_a = sp.TCP(
+ sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
+ seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
+ )
+ req = ether / ip_a / tcp_a
+ sp.sendp(req, iface=send_params['sendif'], verbose=False)
+ return True
+
+ return False
+
+
+def check_udp_request_4(expect_params, packet):
+ if not check_ipv4(expect_params, packet):
+ return False
+ if not check_udp(expect_params, packet):
+ return False
+ return True
+
+
def check_tcp_syn_request_6(expect_params, packet):
if not check_ipv6(expect_params, packet):
return False
@@ -360,7 +471,45 @@ def check_tcp_syn_reply_6(expect_params, packet):
return True
-def check_tcp_syn_request(expect_params, packet):
+def check_tcp_3way_6(args, packet):
+ send_params = args['send_params']
+
+ expect_params_sa = clean_params(args['expect_params'])
+ expect_params_sa['src_address'] = send_params['dst_address']
+ expect_params_sa['dst_address'] = send_params['src_address']
+
+ # Sniff incoming SYN+ACK packet
+ if (
+ check_ipv6(expect_params_sa, packet) and
+ check_tcp(expect_params_sa | {'tcp_flags': 'SA'}, packet)
+ ):
+ ether = sp.Ether()
+ ip6_sa = packet.getlayer(sp.IPv6)
+ tcp_sa = packet.getlayer(sp.TCP)
+ reply_params = clean_params(send_params)
+ reply_params['src_address'] = ip6_sa.dst
+ reply_params['dst_address'] = ip6_sa.src
+ ip_a = prepare_ipv6(reply_params)
+ tcp_a = sp.TCP(
+ sport=tcp_sa.dport, dport=tcp_sa.sport, flags='A',
+ seq=tcp_sa.ack, ack=tcp_sa.seq + 1,
+ )
+ req = ether / ip_a / tcp_a
+ sp.sendp(req, iface=send_params['sendif'], verbose=False)
+ return True
+
+ return False
+
+
+def check_udp_request_6(expect_params, packet):
+ if not check_ipv6(expect_params, packet):
+ return False
+ if not check_udp(expect_params, packet):
+ return False
+ return True
+
+def check_tcp_syn_request(args, packet):
+ expect_params = args['expect_params']
src_address = expect_params.get('src_address')
dst_address = expect_params.get('dst_address')
if not (src_address or dst_address):
@@ -374,7 +523,8 @@ def check_tcp_syn_request(expect_params, packet):
return check_tcp_syn_request_4(expect_params, packet)
-def check_tcp_syn_reply(expect_params, packet):
+def check_tcp_syn_reply(args, packet):
+ expect_params = args['expect_params']
src_address = expect_params.get('src_address')
dst_address = expect_params.get('dst_address')
if not (src_address or dst_address):
@@ -387,8 +537,39 @@ def check_tcp_syn_reply(expect_params, packet):
else:
return check_tcp_syn_reply_4(expect_params, packet)
+def check_tcp_3way(args, packet):
+ expect_params = args['expect_params']
+ src_address = expect_params.get('src_address')
+ dst_address = expect_params.get('dst_address')
+ if not (src_address or dst_address):
+ raise Exception('Source or destination address must be given to match the tcp syn reply!')
+ if (
+ (src_address and ':' in src_address) or
+ (dst_address and ':' in dst_address)
+ ):
+ return check_tcp_3way_6(args, packet)
+ else:
+ return check_tcp_3way_4(args, packet)
+
-def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag):
+def check_udp_request(args, packet):
+ expect_params = args['expect_params']
+ src_address = expect_params.get('src_address')
+ dst_address = expect_params.get('dst_address')
+ if not (src_address or dst_address):
+ raise Exception('Source or destination address must be given to match the tcp syn request!')
+ if (
+ (src_address and ':' in src_address) or
+ (dst_address and ':' in dst_address)
+ ):
+ return check_udp_request_6(expect_params, packet)
+ else:
+ return check_udp_request_4(expect_params, packet)
+
+
+def setup_sniffer(
+ recvif, ping_type, sniff_type, expect_params, defrag, send_params,
+):
if ping_type == 'icmp' and sniff_type == 'request':
checkfn = check_ping_request
elif ping_type == 'icmp' and sniff_type == 'reply':
@@ -397,10 +578,17 @@ def setup_sniffer(recvif, ping_type, sniff_type, expect_params, defrag):
checkfn = check_tcp_syn_request
elif ping_type == 'tcpsyn' and sniff_type == 'reply':
checkfn = check_tcp_syn_reply
+ elif ping_type == 'tcp3way' and sniff_type == 'reply':
+ checkfn = check_tcp_3way
+ elif ping_type == 'udp' and sniff_type == 'request':
+ checkfn = check_udp_request
else:
- raise Exception('Unspported ping or sniff type')
+ raise Exception('Unspported ping and sniff type combination')
- return Sniffer(expect_params, checkfn, recvif, defrag=defrag)
+ return Sniffer(
+ {'send_params': send_params, 'expect_params': expect_params},
+ checkfn, recvif, defrag=defrag,
+ )
def parse_args():
@@ -408,17 +596,15 @@ def parse_args():
description="Ping test tool")
# Parameters of sent ping request
- parser.add_argument('--sendif', nargs=1,
- required=True,
+ parser.add_argument('--sendif', required=True,
help='The interface through which the packet(s) will be sent')
- parser.add_argument('--to', nargs=1,
- required=True,
+ parser.add_argument('--to', required=True,
help='The destination IP address for the ping request')
parser.add_argument('--ping-type',
- choices=('icmp', 'tcpsyn'),
- help='Type of ping: ICMP (default) or TCP SYN',
+ choices=('icmp', 'tcpsyn', 'tcp3way', 'udp'),
+ help='Type of ping: ICMP (default) or TCP SYN or 3-way TCP handshake',
default='icmp')
- parser.add_argument('--fromaddr', nargs=1,
+ parser.add_argument('--fromaddr',
help='The source IP address for the ping request')
# Where to look for packets to analyze.
@@ -431,36 +617,40 @@ def parse_args():
# Packet settings
parser_send = parser.add_argument_group('Values set in transmitted packets')
- parser_send.add_argument('--send-flags', nargs=1, type=str,
+ parser_send.add_argument('--send-flags', type=str,
help='IPv4 fragmentation flags')
- parser_send.add_argument('--send-frag-length', nargs=1, type=int,
- help='Force IP fragmentation with given fragment length')
- parser_send.add_argument('--send-hlim', nargs=1, type=int,
+ parser_send.add_argument('--send-frag-length', type=int,
+ help='Force IP fragmentation with given fragment length')
+ parser_send.add_argument('--send-hlim', type=int,
help='IPv6 Hop Limit or IPv4 Time To Live')
- parser_send.add_argument('--send-mss', nargs=1, type=int,
+ parser_send.add_argument('--send-mss', type=int,
help='TCP Maximum Segment Size')
- parser_send.add_argument('--send-seq', nargs=1, type=int,
+ parser_send.add_argument('--send-seq', type=int,
help='TCP sequence number')
- parser_send.add_argument('--send-length', nargs=1, type=int,
- default=[len(PAYLOAD_MAGIC)], help='ICMP Echo Request payload size')
- parser_send.add_argument('--send-tc', nargs=1, type=int,
+ parser_send.add_argument('--send-sport', type=int,
+ help='TCP source port')
+ parser_send.add_argument('--send-dport', type=int, default=9,
+ help='TCP destination port')
+ parser_send.add_argument('--send-length', type=int, default=len(PAYLOAD_MAGIC),
+ help='ICMP Echo Request payload size')
+ parser_send.add_argument('--send-tc', type=int,
help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
parser_send.add_argument('--send-tcpopt-unaligned', action='store_true',
- help='Include unaligned TCP options')
+ help='Include unaligned TCP options')
parser_send.add_argument('--send-nop', action='store_true',
- help='Include a NOP IPv4 option')
+ help='Include a NOP IPv4 option')
# Expectations
parser_expect = parser.add_argument_group('Values expected in sniffed packets')
- parser_expect.add_argument('--expect-flags', nargs=1, type=str,
+ parser_expect.add_argument('--expect-flags', type=str,
help='IPv4 fragmentation flags')
- parser_expect.add_argument('--expect-hlim', nargs=1, type=int,
+ parser_expect.add_argument('--expect-hlim', type=int,
help='IPv6 Hop Limit or IPv4 Time To Live')
- parser_expect.add_argument('--expect-mss', nargs=1, type=int,
+ parser_expect.add_argument('--expect-mss', type=int,
help='TCP Maximum Segment Size')
- parser_send.add_argument('--expect-seq', nargs=1, type=int,
+ parser_send.add_argument('--expect-seq', type=int,
help='TCP sequence number')
- parser_expect.add_argument('--expect-tc', nargs=1, type=int,
+ parser_expect.add_argument('--expect-tc', type=int,
help='IPv6 Traffic Class or IPv4 DiffServ / ToS')
parser.add_argument('-v', '--verbose', action='store_true',
@@ -478,31 +668,31 @@ def main():
if args.verbose:
LOGGER.setLevel(logging.DEBUG)
- # Dig out real values of program arguments
- send_if = args.sendif[0]
- reply_ifs = args.replyif
- recv_ifs = args.recvif
- dst_address = args.to[0]
-
- # Standardize parameters which have nargs=1.
+ # Split parameters into send and expect parameters. Parameters might be
+ # missing from the command line, always fill the dictionaries with None.
send_params = {}
expect_params = {}
- for param_name in ('flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length'):
+ for param_name in (
+ 'flags', 'hlim', 'length', 'mss', 'seq', 'tc', 'frag_length',
+ 'sport', 'dport',
+ ):
param_arg = vars(args).get(f'send_{param_name}')
- send_params[param_name] = param_arg[0] if param_arg else None
+ send_params[param_name] = param_arg if param_arg else None
param_arg = vars(args).get(f'expect_{param_name}')
- expect_params[param_name] = param_arg[0] if param_arg else None
+ expect_params[param_name] = param_arg if param_arg else None
expect_params['length'] = send_params['length']
send_params['tcpopt_unaligned'] = args.send_tcpopt_unaligned
send_params['nop'] = args.send_nop
- send_params['src_address'] = args.fromaddr[0] if args.fromaddr else None
+ send_params['src_address'] = args.fromaddr if args.fromaddr else None
+ send_params['dst_address'] = args.to
+ send_params['sendif'] = args.sendif
# We may not have a default route. Tell scapy where to start looking for routes
- sp.conf.iface6 = send_if
+ sp.conf.iface6 = args.sendif
# Configuration sanity checking.
- if not (reply_ifs or recv_ifs):
+ if not (args.replyif or args.recvif):
raise Exception('With no reply or recv interface specified no traffic '
'can be sniffed and verified!'
)
@@ -510,35 +700,41 @@ def main():
sniffers = []
if send_params['frag_length']:
- defrag = True
+ if (
+ (send_params['src_address'] and ':' in send_params['src_address']) or
+ (send_params['dst_address'] and ':' in send_params['dst_address'])
+ ):
+ defrag = 'IPv6'
+ else:
+ defrag = 'IPv4'
else:
defrag = False
- if recv_ifs:
+ if args.recvif:
sniffer_params = copy(expect_params)
sniffer_params['src_address'] = None
- sniffer_params['dst_address'] = dst_address
- for iface in recv_ifs:
+ sniffer_params['dst_address'] = args.to
+ for iface in args.recvif:
LOGGER.debug(f'Installing receive sniffer on {iface}')
sniffers.append(
setup_sniffer(iface, args.ping_type, 'request',
- sniffer_params, defrag,
+ sniffer_params, defrag, send_params,
))
- if reply_ifs:
+ if args.replyif:
sniffer_params = copy(expect_params)
- sniffer_params['src_address'] = dst_address
+ sniffer_params['src_address'] = args.to
sniffer_params['dst_address'] = None
- for iface in reply_ifs:
+ for iface in args.replyif:
LOGGER.debug(f'Installing reply sniffer on {iface}')
sniffers.append(
setup_sniffer(iface, args.ping_type, 'reply',
- sniffer_params, defrag,
+ sniffer_params, defrag, send_params,
))
LOGGER.debug(f'Installed {len(sniffers)} sniffers')
- send_ping(dst_address, send_if, args.ping_type, send_params)
+ send_ping(args.ping_type, send_params)
err = 0
sniffer_num = 0
diff --git a/tests/sys/netpfil/common/rdr.sh b/tests/sys/netpfil/common/rdr.sh
index 7d6297870e6c..0d6f27694c8c 100644
--- a/tests/sys/netpfil/common/rdr.sh
+++ b/tests/sys/netpfil/common/rdr.sh
@@ -83,6 +83,7 @@ local_redirect_body()
firewall=$1
firewall_init $firewall
nat_init $firewall
+ vnet_init_bridge
bridge=$(vnet_mkbridge)
ifconfig ${bridge} 192.0.2.1/24 up
diff --git a/tests/sys/netpfil/common/sniffer.py b/tests/sys/netpfil/common/sniffer.py
index 14305a37278c..583b27d34ca6 100644
--- a/tests/sys/netpfil/common/sniffer.py
+++ b/tests/sys/netpfil/common/sniffer.py
@@ -56,14 +56,19 @@ class Sniffer(threading.Thread):
def run(self):
self.packets = []
- if self._defrag:
- # With fragment reassembly we can't stop the sniffer after catching
- # the good packets, as those have not been reassembled. We must
- # wait for sniffer to finish and check returned packets instead.
+ # With fragment reassembly we can't stop the sniffer after catching
+ # the good packets, as those have not been reassembled. We must
+ # wait for sniffer to finish and check returned packets instead.
+ if self._defrag == 'IPv4':
self.packets = sp.sniff(session=sp.IPSession, iface=self._recvif,
timeout=self._timeout, started_callback=self._startedCb)
for p in self.packets:
self._checkPacket(p)
+ elif self._defrag == 'IPv6':
+ self.packets = sp.sniff(session=sp.DefaultSession, iface=self._recvif,
+ timeout=self._timeout, started_callback=self._startedCb)
+ for p in sp.defragment6(self.packets):
+ self._checkPacket(p)
else:
self.packets = sp.sniff(iface=self._recvif,
stop_filter=self._checkPacket, timeout=self._timeout,
diff --git a/tests/sys/netpfil/common/tos.sh b/tests/sys/netpfil/common/tos.sh
index 39f756be8fe9..3b689d7f67d0 100644
--- a/tests/sys/netpfil/common/tos.sh
+++ b/tests/sys/netpfil/common/tos.sh
@@ -33,7 +33,7 @@ tos_head()
{
atf_set descr 'set-tos test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
tos_body()
diff --git a/tests/sys/netpfil/ipfw/Makefile b/tests/sys/netpfil/ipfw/Makefile
index 1d4629c0e738..d4dbdb00f251 100644
--- a/tests/sys/netpfil/ipfw/Makefile
+++ b/tests/sys/netpfil/ipfw/Makefile
@@ -2,7 +2,13 @@ PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netpfil/ipfw
-ATF_TESTS_SH+= fwd
+ATF_TESTS_SH+= fwd \
+ divert
+
${PACKAGE}FILES+= fwd_inetd.conf
+# Allow tests to run in parallel in their own jails
+TEST_METADATA+= execenv="jail"
+TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets"
+
.include <bsd.test.mk>
diff --git a/tests/sys/netpfil/ipfw/divert.sh b/tests/sys/netpfil/ipfw/divert.sh
new file mode 100644
index 000000000000..62db3f8fce98
--- /dev/null
+++ b/tests/sys/netpfil/ipfw/divert.sh
@@ -0,0 +1,281 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+#
+# ipfw divert action test cases
+#
+# -----------| |-- |----| ----| |-----------
+# ( ) inbound |ipfw| ) -> |host| -> ( ) |ipfw| outbound )
+# -----------| | |-- |----| ----| | |-----------
+# | |
+# \|/ \|/
+# |------| |------|
+# |divapp| |divapp|
+# |------| |------|
+#
+# The basic cases:
+# - inbound > diverted | divapp terminated
+# - inbound > diverted > inbound | host terminated
+# - inbound > diverted > outbound | network terminated
+# - outbound > diverted | divapp terminated
+# - outbound > diverted > outbound | network terminated
+# - outbound > diverted > inbound | e.g. host terminated
+#
+# When a packet is diverted, forwarded, and possibly diverted again:
+# - inbound > diverted > inbound > forwarded
+# > outbound | network terminated
+# - inbound > diverted > inbound > forwarded
+# > outbound > diverted > outbound | network terminated
+#
+# Test case naming legend:
+# in - inbound
+# div - diverted
+# out - outbound
+# fwd - forwarded
+#
+
+. $(atf_get_srcdir)/../common/utils.subr
+
+divert_init()
+{
+ if ! kldstat -q -m ipdivert; then
+ atf_skip "This test requires ipdivert"
+ fi
+}
+
+atf_test_case "in_div" "cleanup"
+in_div_head()
+{
+ atf_set descr 'Test inbound > diverted | divapp terminated'
+ atf_set require.user root
+}
+in_div_body()
+{
+ firewall_init "ipfw"
+ divert_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail div ${epair}b
+ ifconfig ${epair}a 192.0.2.1/24 up
+ jexec div ifconfig ${epair}b 192.0.2.2/24 up
+ jexec div ipfw add 65534 allow all from any to any
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+ jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8
+
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 &
+ divapp_pid=$!
+ # Wait for the divapp to be ready
+ sleep 1
+
+ # divapp is expected to "eat" the packet
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
+
+ wait $divapp_pid
+}
+in_div_cleanup()
+{
+ firewall_cleanup "ipfw"
+}
+
+atf_test_case "in_div_in" "cleanup"
+in_div_in_head()
+{
+ atf_set descr 'Test inbound > diverted > inbound | host terminated'
+ atf_set require.user root
+}
+in_div_in_body()
+{
+ firewall_init "ipfw"
+ divert_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail div ${epair}b
+ ifconfig ${epair}a 192.0.2.1/24 up
+ jexec div ifconfig ${epair}b 192.0.2.2/24 up
+ jexec div ipfw add 65534 allow all from any to any
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+ jexec div ipfw add 100 divert 2000 icmp from any to any in icmptypes 8
+
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back &
+ divapp_pid=$!
+ # Wait for the divapp to be ready
+ sleep 1
+
+ # divapp is NOT expected to "eat" the packet
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ wait $divapp_pid
+}
+in_div_in_cleanup()
+{
+ firewall_cleanup "ipfw"
+}
+
+atf_test_case "out_div" "cleanup"
+out_div_head()
+{
+ atf_set descr 'Test outbound > diverted | divapp terminated'
+ atf_set require.user root
+}
+out_div_body()
+{
+ firewall_init "ipfw"
+ divert_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail div ${epair}b
+ ifconfig ${epair}a 192.0.2.1/24 up
+ jexec div ifconfig ${epair}b 192.0.2.2/24 up
+ jexec div ipfw add 65534 allow all from any to any
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+ jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0
+
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 &
+ divapp_pid=$!
+ # Wait for the divapp to be ready
+ sleep 1
+
+ # divapp is expected to "eat" the packet
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
+
+ wait $divapp_pid
+}
+out_div_cleanup()
+{
+ firewall_cleanup "ipfw"
+}
+
+atf_test_case "out_div_out" "cleanup"
+out_div_out_head()
+{
+ atf_set descr 'Test outbound > diverted > outbound | network terminated'
+ atf_set require.user root
+}
+out_div_out_body()
+{
+ firewall_init "ipfw"
+ divert_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail div ${epair}b
+ ifconfig ${epair}a 192.0.2.1/24 up
+ jexec div ifconfig ${epair}b 192.0.2.2/24 up
+ jexec div ipfw add 65534 allow all from any to any
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+ jexec div ipfw add 100 divert 2000 icmp from any to any out icmptypes 0
+
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back &
+ divapp_pid=$!
+ # Wait for the divapp to be ready
+ sleep 1
+
+ # divapp is NOT expected to "eat" the packet
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ wait $divapp_pid
+}
+out_div_out_cleanup()
+{
+ firewall_cleanup "ipfw"
+}
+
+atf_test_case "in_div_in_fwd_out_div_out" "cleanup"
+in_div_in_fwd_out_div_out_head()
+{
+ atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated'
+ atf_set require.user root
+}
+in_div_in_fwd_out_div_out_body()
+{
+ firewall_init "ipfw"
+ divert_init
+
+ # host <a--epair0--b> router <a--epair1--b> site
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+
+ vnet_mkjail router ${epair0}b ${epair1}a
+ ifconfig ${epair0}a 192.0.2.1/24 up
+ jexec router sysctl net.inet.ip.forwarding=1
+ jexec router ifconfig ${epair0}b 192.0.2.2/24 up
+ jexec router ifconfig ${epair1}a 198.51.100.1/24 up
+ jexec router ipfw add 65534 allow all from any to any
+
+ vnet_mkjail site ${epair1}b
+ jexec site ifconfig ${epair1}b 198.51.100.2/24 up
+ jexec site ipfw add 65534 allow all from any to any
+ jexec site route add default 198.51.100.1
+
+ route add -net 198.51.100.0/24 192.0.2.2
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+ # Should be routed without diversion
+ atf_check -s exit:0 -o ignore ping -c3 198.51.100.2
+
+ jexec router ipfw add 100 divert 2001 icmp from any to any in icmptypes 8
+ jexec router ipfw add 200 divert 2002 icmp from any to any out icmptypes 8
+
+ jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back &
+ indivapp_pid=$!
+ jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back &
+ outdivapp_pid=$!
+ # Wait for the divappS to be ready
+ sleep 1
+
+ # Both divappS are NOT expected to "eat" the packet
+ atf_check -s exit:0 -o ignore ping -c1 198.51.100.2
+
+ wait $indivapp_pid && wait $outdivapp_pid
+}
+in_div_in_fwd_out_div_out_cleanup()
+{
+ firewall_cleanup "ipfw"
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "in_div"
+ atf_add_test_case "in_div_in"
+
+ atf_add_test_case "out_div"
+ atf_add_test_case "out_div_out"
+
+ atf_add_test_case "in_div_in_fwd_out_div_out"
+}
diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile
index 867b98e5f6c2..3adaef09ddbd 100644
--- a/tests/sys/netpfil/pf/Makefile
+++ b/tests/sys/netpfil/pf/Makefile
@@ -1,12 +1,11 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netpfil/pf
-BINDIR= ${TESTSDIR}
TESTS_SUBDIRS+= ioctl
ATF_TESTS_SH+= altq \
anchor \
+ debug \
divert-to \
dup \
ether \
@@ -16,14 +15,21 @@ ATF_TESTS_SH+= altq \
fragmentation_no_reassembly \
get_state \
icmp \
+ icmp6 \
+ if_enc \
+ limits \
loginterface \
killstate \
macro \
- map_e \
match \
+ max_pkt_rate \
+ max_pkt_size \
+ max_states \
+ mbuf \
modulate \
names \
nat \
+ nat64 \
pass_block \
pflog \
pflow \
@@ -40,7 +46,9 @@ ATF_TESTS_SH+= altq \
sctp \
set_skip \
set_tos \
+ snmp \
src_track \
+ status \
syncookie \
synproxy \
table \
@@ -48,15 +56,21 @@ ATF_TESTS_SH+= altq \
tos
ATF_TESTS_PYTEST+= frag6.py
+ATF_TESTS_PYTEST+= header.py
+ATF_TESTS_PYTEST+= icmp.py
+ATF_TESTS_PYTEST+= nat64.py
ATF_TESTS_PYTEST+= nat66.py
+ATF_TESTS_PYTEST+= return.py
ATF_TESTS_PYTEST+= sctp.py
+ATF_TESTS_PYTEST+= tcp.py
-# Tests reuse jail names and so cannot run in parallel.
-TEST_METADATA+= is_exclusive=true
-
-PROGS= divapp
+# Allow tests to run in parallel in their own jails
+TEST_METADATA+= execenv="jail"
+TEST_METADATA+= execenv_jail_params="vnet allow.raw_sockets"
-${PACKAGE}FILES+= CVE-2019-5597.py \
+${PACKAGE}FILES+= \
+ bsnmpd.conf \
+ CVE-2019-5597.py \
CVE-2019-5598.py \
daytime_inetd.conf \
echo_inetd.conf \
@@ -64,17 +78,24 @@ ${PACKAGE}FILES+= CVE-2019-5597.py \
frag-overindex.py \
frag-overlimit.py \
frag-overreplace.py \
+ frag-overhole.py \
+ frag-adjhole.py \
pfsync_defer.py \
pft_ether.py \
pft_read_ipfix.py \
- utils.subr
+ rdr-srcport.py \
+ utils.subr \
+ utils.py
+${PACKAGE}FILESMODE_bsnmpd.conf= 0555
${PACKAGE}FILESMODE_CVE-2019-5597.py= 0555
${PACKAGE}FILESMODE_CVE-2019-5598.py= 0555
${PACKAGE}FILESMODE_fragcommon.py= 0555
${PACKAGE}FILESMODE_frag-overindex.py= 0555
${PACKAGE}FILESMODE_frag-overlimit.py= 0555
${PACKAGE}FILESMODE_frag-overreplace.py= 0555
+${PACKAGE}FILESMODE_frag-overhole.py= 0555
+${PACKAGE}FILESMODE_frag-adjhole.py= 0555
${PACKAGE}FILESMODE_pfsync_defer.py= 0555
${PACKAGE}FILESMODE_pft_ether.py= 0555
${PACKAGE}FILESMODE_pft_read_ipfix.py= 0555
diff --git a/tests/sys/netpfil/pf/altq.sh b/tests/sys/netpfil/pf/altq.sh
index 57a9edf11366..416a55777849 100644
--- a/tests/sys/netpfil/pf/altq.sh
+++ b/tests/sys/netpfil/pf/altq.sh
@@ -156,9 +156,7 @@ codel_bridge_body()
{
altq_init
is_altq_supported codel
- if ! kldstat -q -m if_bridge; then
- atf_skip "This test requires if_bridge"
- fi
+ vnet_init_bridge
epair=$(vnet_mkepair)
ifconfig ${epair}a 192.0.2.1/24 up
@@ -214,7 +212,7 @@ prioritise_body()
ifconfig ${epair}a 192.0.2.1/24 up
jexec altq_prioritise ifconfig ${epair}b 192.0.2.2/24 up
- jexec altq_prioritise /usr/sbin/inetd -p inetd-altq.pid \
+ jexec altq_prioritise /usr/sbin/inetd -p ${PWD}/inetd-altq.pid \
$(atf_get_srcdir)/../pf/echo_inetd.conf
# Sanity check
diff --git a/tests/sys/netpfil/pf/anchor.sh b/tests/sys/netpfil/pf/anchor.sh
index b1faa5f6c57e..b4b52d7a24d6 100644
--- a/tests/sys/netpfil/pf/anchor.sh
+++ b/tests/sys/netpfil/pf/anchor.sh
@@ -58,6 +58,37 @@ pr183198_cleanup()
pft_cleanup
}
+atf_test_case "pr279225" "cleanup"
+pr279225_head()
+{
+ atf_set descr "Test that we can retrieve longer anchor names, PR 279225"
+ atf_set require.user root
+}
+
+pr279225_body()
+{
+ pft_init
+
+ vnet_mkjail alcatraz
+
+ pft_set_rules alcatraz \
+ "nat-anchor \"appjail-nat/jail/*\" all" \
+ "rdr-anchor \"appjail-rdr/*\" all" \
+ "anchor \"appjail/jail/*\" all"
+
+ atf_check -s exit:0 -o match:"nat-anchor \"appjail-nat/jail/\*\" all \{" \
+ jexec alcatraz pfctl -sn -a "*"
+ atf_check -s exit:0 -o match:"rdr-anchor \"appjail-rdr/\*\" all \{" \
+ jexec alcatraz pfctl -sn -a "*"
+ atf_check -s exit:0 -o match:"anchor \"appjail/jail/\*\" all \{" \
+ jexec alcatraz pfctl -sr -a "*"
+}
+
+pr279225_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "nested_anchor" "cleanup"
nested_anchor_head()
{
@@ -164,10 +195,259 @@ nested_label_cleanup()
pft_cleanup
}
+atf_test_case "quick" "cleanup"
+quick_head()
+{
+ atf_set descr "Test handling of quick on anchors"
+ atf_set require.user root
+}
+
+quick_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "anchor quick {\n\
+ pass\n\
+ }" \
+ "block"
+
+ # We can still ping because the anchor is 'quick'
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+ jexec alcatraz pfctl -sr -v
+ jexec alcatraz pfctl -ss -v
+}
+
+quick_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "quick_nested" "cleanup"
+quick_nested_head()
+{
+ atf_set descr 'Verify that a nested anchor does not clear quick'
+ atf_set require.user root
+}
+
+quick_nested_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "anchor quick {\n\
+ pass\n\
+ anchor {\n\
+ block proto tcp\n\
+ }\n\
+ }" \
+ "block"
+ ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -sr -v
+ jexec alcatraz pfctl -ss -v
+
+ # We can still ping because the anchor is 'quick'
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+ jexec alcatraz pfctl -sr -v
+ jexec alcatraz pfctl -ss -v
+}
+
+quick_nested_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "counter" "cleanup"
+counter_head()
+{
+ atf_set descr 'Test counters on anchors'
+ atf_set require.user root
+}
+
+counter_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "anchor \"foo\" {\n\
+ pass\n\
+ }"
+
+ # Generate traffic
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+ atf_check -s exit:0 -e ignore \
+ -o match:'[ Evaluations: 1 Packets: 2 Bytes: 168 States: 1 ]' \
+ jexec alcatraz pfctl -sr -vv
+}
+
+counter_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "nat" "cleanup"
+nat_head()
+{
+ atf_set descr 'Test nested nat anchors'
+ atf_set require.user root
+}
+
+nat_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "nat-anchor \"foo/*\"" \
+ "pass"
+
+ echo "nat log on ${epair}a inet from 192.0.2.0/24 to any port = 53 -> 192.0.2.1" \
+ | jexec alcatraz pfctl -a "foo/bar" -g -f -
+ echo "rdr on ${epair}a proto tcp to port echo -> 127.0.0.1 port echo" \
+ | jexec alcatraz pfctl -a "foo/baz" -g -f -
+
+ jexec alcatraz pfctl -sn -a "*"
+ jexec alcatraz pfctl -sn -a "foo/bar"
+ jexec alcatraz pfctl -sn -a "foo/baz"
+
+ atf_check -s exit:0 -o match:"nat log on epair0a inet from 192.0.2.0/24 to any port = domain -> 192.0.2.1" \
+ jexec alcatraz pfctl -sn -a "*"
+ atf_check -s exit:0 -o match:"rdr on epair0a inet proto tcp from any to any port = echo -> 127.0.0.1 port 7" \
+ jexec alcatraz pfctl -sn -a "*"
+}
+
+nat_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "include" "cleanup"
+include_head()
+{
+ atf_set descr 'Test including inside anchors'
+ atf_set require.user root
+}
+
+include_body()
+{
+ pft_init
+
+ wd=`pwd`
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ echo "pass" > ${wd}/extra.conf
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block" \
+ "anchor \"foo\" {\n\
+ include \"${wd}/extra.conf\"\n\
+ }"
+
+ jexec alcatraz pfctl -sr
+
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+}
+
+include_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "quick" "cleanup"
+quick_head()
+{
+ atf_set descr 'Test quick on anchors'
+ atf_set require.user root
+}
+
+quick_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "anchor quick {\n\
+ pass\n\
+ }" \
+ "block"
+
+ atf_check -s exit:0 -o ignore ping -c 1 192.0.2.1
+ jexec alcatraz pfctl -sr -vv -a "*"
+}
+
+quick_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "pr183198"
+ atf_add_test_case "pr279225"
atf_add_test_case "nested_anchor"
atf_add_test_case "wildcard"
atf_add_test_case "nested_label"
+ atf_add_test_case "quick"
+ atf_add_test_case "quick_nested"
+ atf_add_test_case "counter"
+ atf_add_test_case "nat"
+ atf_add_test_case "include"
+ atf_add_test_case "quick"
}
diff --git a/tests/sys/netpfil/pf/bsnmpd.conf b/tests/sys/netpfil/pf/bsnmpd.conf
new file mode 100644
index 000000000000..27abdda6cbd3
--- /dev/null
+++ b/tests/sys/netpfil/pf/bsnmpd.conf
@@ -0,0 +1,47 @@
+location := "A galaxy far, far away"
+contact := "skywalker@Tatooine"
+system := 1
+
+read := "public"
+write := "geheim"
+trap := "mytrap"
+
+NoAuthProtocol := 1.3.6.1.6.3.10.1.1.1
+HMACMD5AuthProtocol := 1.3.6.1.6.3.10.1.1.2
+HMACSHAAuthProtocol := 1.3.6.1.6.3.10.1.1.3
+NoPrivProtocol := 1.3.6.1.6.3.10.1.2.1
+DESPrivProtocol := 1.3.6.1.6.3.10.1.2.2
+AesCfb128Protocol := 1.3.6.1.6.3.10.1.2.4
+
+securityModelAny := 0
+securityModelSNMPv1 := 1
+securityModelSNMPv2c := 2
+securityModelUSM := 3
+
+MPmodelSNMPv1 := 0
+MPmodelSNMPv2c := 1
+MPmodelSNMPv3 := 3
+
+noAuthNoPriv := 1
+authNoPriv := 2
+authPriv := 3
+
+%snmpd
+begemotSnmpdDebugDumpPdus = 2
+begemotSnmpdDebugSyslogPri = 7
+
+begemotSnmpdCommunityString.0.1 = $(read)
+begemotSnmpdCommunityDisable = 1
+
+begemotSnmpdTransInetStatus.1.4.0.0.0.0.161.1 = 4
+begemotSnmpdTransInetStatus.2.16.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.161.1 = 4
+
+begemotSnmpdLocalPortStatus."/var/run/snmpd.sock" = 1
+begemotSnmpdLocalPortType."/var/run/snmpd.sock" = 4
+
+sysContact = $(contact)
+sysLocation = $(location)
+sysObjectId = 1.3.6.1.4.1.12325.1.1.2.1.$(system)
+
+begemotSnmpdModulePath."mibII" = "/usr/lib/snmp_mibII.so"
+begemotSnmpdModulePath."pf" = "/usr/lib/snmp_pf.so"
diff --git a/tests/sys/netpfil/pf/debug.sh b/tests/sys/netpfil/pf/debug.sh
new file mode 100644
index 000000000000..18a7febfbb5b
--- /dev/null
+++ b/tests/sys/netpfil/pf/debug.sh
@@ -0,0 +1,56 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Test setting and retrieving debug level'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ vnet_mkjail debug
+ atf_check -s exit:0 -e ignore \
+ jexec debug pfctl -x loud
+
+ atf_check -s exit:0 -o match:'Debug: Loud' \
+ jexec debug pfctl -si
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+}
diff --git a/tests/sys/netpfil/pf/divert-to.sh b/tests/sys/netpfil/pf/divert-to.sh
index 72adbeedb007..ae44cd5d51af 100644
--- a/tests/sys/netpfil/pf/divert-to.sh
+++ b/tests/sys/netpfil/pf/divert-to.sh
@@ -51,8 +51,6 @@
# > outbound > diverted > outbound | network terminated
#
# Test case naming legend:
-# ipfwon - with ipfw enabled
-# ipfwoff - with ipfw disabled
# in - inbound
# div - diverted
# out - outbound
@@ -76,40 +74,21 @@ dummynet_init()
fi
}
-ipfw_init()
-{
- if ! kldstat -q -m ipfw; then
- atf_skip "This test requires ipfw"
- fi
-}
-
-assert_ipfw_is_off()
-{
- if kldstat -q -m ipfw; then
- atf_skip "This test is for the case when ipfw is not loaded"
- fi
-}
-
-atf_test_case "ipfwoff_in_div" "cleanup"
-ipfwoff_in_div_head()
+atf_test_case "in_div" "cleanup"
+in_div_head()
{
atf_set descr 'Test inbound > diverted | divapp terminated'
atf_set require.user root
}
-ipfwoff_in_div_body()
+in_div_body()
{
- local ipfwon
-
pft_init
divert_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
epair=$(vnet_mkepair)
vnet_mkjail div ${epair}b
ifconfig ${epair}a 192.0.2.1/24 up
jexec div ifconfig ${epair}b 192.0.2.2/24 up
- test $ipfwon && jexec div ipfw add 65534 allow all from any to any
# Sanity check
atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
@@ -119,56 +98,36 @@ ipfwoff_in_div_body()
"pass all" \
"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000"
- jexec div $(atf_get_srcdir)/divapp 2000 &
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 &
divapp_pid=$!
# Wait for the divapp to be ready
sleep 1
# divapp is expected to "eat" the packet
- atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
wait $divapp_pid
}
-ipfwoff_in_div_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwon_in_div" "cleanup"
-ipfwon_in_div_head()
-{
- atf_set descr 'Test inbound > diverted | divapp terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_in_div_body()
-{
- ipfwoff_in_div_body "ipfwon"
-}
-ipfwon_in_div_cleanup()
+in_div_cleanup()
{
pft_cleanup
}
-atf_test_case "ipfwoff_in_div_in" "cleanup"
-ipfwoff_in_div_in_head()
+atf_test_case "in_div_in" "cleanup"
+in_div_in_head()
{
atf_set descr 'Test inbound > diverted > inbound | host terminated'
atf_set require.user root
}
-ipfwoff_in_div_in_body()
+in_div_in_body()
{
- local ipfwon
-
pft_init
divert_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
epair=$(vnet_mkepair)
vnet_mkjail div ${epair}b
ifconfig ${epair}a 192.0.2.1/24 up
jexec div ifconfig ${epair}b 192.0.2.2/24 up
- test $ipfwon && jexec div ipfw add 65534 allow all from any to any
# Sanity check
atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
@@ -178,7 +137,7 @@ ipfwoff_in_div_in_body()
"pass all" \
"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000 no state"
- jexec div $(atf_get_srcdir)/divapp 2000 divert-back &
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back &
divapp_pid=$!
# Wait for the divapp to be ready
sleep 1
@@ -188,46 +147,26 @@ ipfwoff_in_div_in_body()
wait $divapp_pid
}
-ipfwoff_in_div_in_cleanup()
+in_div_in_cleanup()
{
pft_cleanup
}
-atf_test_case "ipfwon_in_div_in" "cleanup"
-ipfwon_in_div_in_head()
-{
- atf_set descr 'Test inbound > diverted > inbound | host terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_in_div_in_body()
-{
- ipfwoff_in_div_in_body "ipfwon"
-}
-ipfwon_in_div_in_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwoff_out_div" "cleanup"
-ipfwoff_out_div_head()
+atf_test_case "out_div" "cleanup"
+out_div_head()
{
atf_set descr 'Test outbound > diverted | divapp terminated'
atf_set require.user root
}
-ipfwoff_out_div_body()
+out_div_body()
{
- local ipfwon
-
pft_init
divert_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
epair=$(vnet_mkepair)
vnet_mkjail div ${epair}b
ifconfig ${epair}a 192.0.2.1/24 up
jexec div ifconfig ${epair}b 192.0.2.2/24 up
- test $ipfwon && jexec div ipfw add 65534 allow all from any to any
# Sanity check
atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
@@ -238,56 +177,36 @@ ipfwoff_out_div_body()
"pass in inet proto icmp icmp-type echoreq no state" \
"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state"
- jexec div $(atf_get_srcdir)/divapp 2000 &
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 &
divapp_pid=$!
# Wait for the divapp to be ready
sleep 1
# divapp is expected to "eat" the packet
- atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
wait $divapp_pid
}
-ipfwoff_out_div_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwon_out_div" "cleanup"
-ipfwon_out_div_head()
-{
- atf_set descr 'Test outbound > diverted | divapp terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_out_div_body()
-{
- ipfwoff_out_div_body "ipfwon"
-}
-ipfwon_out_div_cleanup()
+out_div_cleanup()
{
pft_cleanup
}
-atf_test_case "ipfwoff_out_div_out" "cleanup"
-ipfwoff_out_div_out_head()
+atf_test_case "out_div_out" "cleanup"
+out_div_out_head()
{
atf_set descr 'Test outbound > diverted > outbound | network terminated'
atf_set require.user root
}
-ipfwoff_out_div_out_body()
+out_div_out_body()
{
- local ipfwon
-
pft_init
divert_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
epair=$(vnet_mkepair)
vnet_mkjail div ${epair}b
ifconfig ${epair}a 192.0.2.1/24 up
jexec div ifconfig ${epair}b 192.0.2.2/24 up
- test $ipfwon && jexec div ipfw add 65534 allow all from any to any
# Sanity check
atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
@@ -298,7 +217,7 @@ ipfwoff_out_div_out_body()
"pass in inet proto icmp icmp-type echoreq no state" \
"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state"
- jexec div $(atf_get_srcdir)/divapp 2000 divert-back &
+ jexec div $(atf_get_srcdir)/../common/divapp 2000 divert-back &
divapp_pid=$!
# Wait for the divapp to be ready
sleep 1
@@ -308,40 +227,21 @@ ipfwoff_out_div_out_body()
wait $divapp_pid
}
-ipfwoff_out_div_out_cleanup()
+out_div_out_cleanup()
{
pft_cleanup
}
-atf_test_case "ipfwon_out_div_out" "cleanup"
-ipfwon_out_div_out_head()
-{
- atf_set descr 'Test outbound > diverted > outbound | network terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_out_div_out_body()
-{
- ipfwoff_out_div_out_body "ipfwon"
-}
-ipfwon_out_div_out_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwoff_in_div_in_fwd_out_div_out" "cleanup"
-ipfwoff_in_div_in_fwd_out_div_out_head()
+atf_test_case "in_div_in_fwd_out_div_out" "cleanup"
+in_div_in_fwd_out_div_out_head()
{
atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated'
atf_set require.user root
}
-ipfwoff_in_div_in_fwd_out_div_out_body()
+in_div_in_fwd_out_div_out_body()
{
- local ipfwon
-
pft_init
divert_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
# host <a--epair0--b> router <a--epair1--b> site
epair0=$(vnet_mkepair)
@@ -352,12 +252,10 @@ ipfwoff_in_div_in_fwd_out_div_out_body()
jexec router sysctl net.inet.ip.forwarding=1
jexec router ifconfig ${epair0}b 192.0.2.2/24 up
jexec router ifconfig ${epair1}a 198.51.100.1/24 up
- test $ipfwon && jexec router ipfw add 65534 allow all from any to any
vnet_mkjail site ${epair1}b
jexec site ifconfig ${epair1}b 198.51.100.2/24 up
jexec site route add default 198.51.100.1
- test $ipfwon && jexec site ipfw add 65534 allow all from any to any
route add -net 198.51.100.0/24 192.0.2.2
@@ -373,9 +271,9 @@ ipfwoff_in_div_in_fwd_out_div_out_body()
"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \
"pass out inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2002 no state"
- jexec router $(atf_get_srcdir)/divapp 2001 divert-back &
+ jexec router $(atf_get_srcdir)/../common/divapp 2001 divert-back &
indivapp_pid=$!
- jexec router $(atf_get_srcdir)/divapp 2002 divert-back &
+ jexec router $(atf_get_srcdir)/../common/divapp 2002 divert-back &
outdivapp_pid=$!
# Wait for the divappS to be ready
sleep 1
@@ -385,48 +283,28 @@ ipfwoff_in_div_in_fwd_out_div_out_body()
wait $indivapp_pid && wait $outdivapp_pid
}
-ipfwoff_in_div_in_fwd_out_div_out_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwon_in_div_in_fwd_out_div_out" "cleanup"
-ipfwon_in_div_in_fwd_out_div_out_head()
-{
- atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_in_div_in_fwd_out_div_out_body()
-{
- ipfwoff_in_div_in_fwd_out_div_out_body "ipfwon"
-}
-ipfwon_in_div_in_fwd_out_div_out_cleanup()
+in_div_in_fwd_out_div_out_cleanup()
{
pft_cleanup
}
-atf_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out" "cleanup"
-ipfwoff_in_dn_in_div_in_out_div_out_dn_out_head()
+atf_test_case "in_dn_in_div_in_out_div_out_dn_out" "cleanup"
+in_dn_in_div_in_out_div_out_dn_out_head()
{
atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated'
atf_set require.user root
}
-ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body()
+in_dn_in_div_in_out_div_out_dn_out_body()
{
- local ipfwon
-
pft_init
divert_init
dummynet_init
- test "$1" == "ipfwon" && ipfwon="yes"
- test $ipfwon && ipfw_init || assert_ipfw_is_off
epair=$(vnet_mkepair)
vnet_mkjail alcatraz ${epair}b
ifconfig ${epair}a 192.0.2.1/24 up
ifconfig ${epair}a ether 02:00:00:00:00:01
jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
- test $ipfwon && jexec alcatraz ipfw add 65534 allow all from any to any
# Sanity check
atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
@@ -444,9 +322,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body()
"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 1001 no state" \
"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 1002 no state"
- jexec alcatraz $(atf_get_srcdir)/divapp 1001 divert-back &
+ jexec alcatraz $(atf_get_srcdir)/../common/divapp 1001 divert-back &
indivapp_pid=$!
- jexec alcatraz $(atf_get_srcdir)/divapp 1002 divert-back &
+ jexec alcatraz $(atf_get_srcdir)/../common/divapp 1002 divert-back &
outdivapp_pid=$!
# Wait for the divappS to be ready
sleep 1
@@ -473,9 +351,9 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body()
"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \
"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2002 no state"
- jexec alcatraz $(atf_get_srcdir)/divapp 2001 divert-back &
+ jexec alcatraz $(atf_get_srcdir)/../common/divapp 2001 divert-back &
indivapp_pid=$!
- jexec alcatraz $(atf_get_srcdir)/divapp 2002 divert-back &
+ jexec alcatraz $(atf_get_srcdir)/../common/divapp 2002 divert-back &
outdivapp_pid=$!
# Wait for the divappS to be ready
sleep 1
@@ -489,41 +367,20 @@ ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body()
# }
}
-ipfwoff_in_dn_in_div_in_out_div_out_dn_out_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out" "cleanup"
-ipfwon_in_dn_in_div_in_out_div_out_dn_out_head()
-{
- atf_set descr 'Test inbound > delayed+diverted > outbound > diverted+delayed > outbound | network terminated, with ipfw enabled'
- atf_set require.user root
-}
-ipfwon_in_dn_in_div_in_out_div_out_dn_out_body()
-{
- ipfwoff_in_dn_in_div_in_out_div_out_dn_out_body "ipfwon"
-}
-ipfwon_in_dn_in_div_in_out_div_out_dn_out_cleanup()
+in_dn_in_div_in_out_div_out_dn_out_cleanup()
{
pft_cleanup
}
atf_init_test_cases()
{
- atf_add_test_case "ipfwoff_in_div"
- atf_add_test_case "ipfwoff_in_div_in"
- atf_add_test_case "ipfwon_in_div"
- atf_add_test_case "ipfwon_in_div_in"
+ atf_add_test_case "in_div"
+ atf_add_test_case "in_div_in"
- atf_add_test_case "ipfwoff_out_div"
- atf_add_test_case "ipfwoff_out_div_out"
- atf_add_test_case "ipfwon_out_div"
- atf_add_test_case "ipfwon_out_div_out"
+ atf_add_test_case "out_div"
+ atf_add_test_case "out_div_out"
- atf_add_test_case "ipfwoff_in_div_in_fwd_out_div_out"
- atf_add_test_case "ipfwon_in_div_in_fwd_out_div_out"
+ atf_add_test_case "in_div_in_fwd_out_div_out"
- atf_add_test_case "ipfwoff_in_dn_in_div_in_out_div_out_dn_out"
- atf_add_test_case "ipfwon_in_dn_in_div_in_out_div_out_dn_out"
+ atf_add_test_case "in_dn_in_div_in_out_div_out_dn_out"
}
diff --git a/tests/sys/netpfil/pf/dup.sh b/tests/sys/netpfil/pf/dup.sh
index 68631251144a..64a08083bca0 100644
--- a/tests/sys/netpfil/pf/dup.sh
+++ b/tests/sys/netpfil/pf/dup.sh
@@ -33,7 +33,7 @@ dup_to_head()
{
atf_set descr 'dup-to test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
dup_to_body()
diff --git a/tests/sys/netpfil/pf/ether.sh b/tests/sys/netpfil/pf/ether.sh
index 9a1ab1b005d7..f0fdce50a7d3 100644
--- a/tests/sys/netpfil/pf/ether.sh
+++ b/tests/sys/netpfil/pf/ether.sh
@@ -286,7 +286,7 @@ captive_body()
# Run the echo server only on the gw, so we know we've redirectly
# correctly if we get an echo message.
- jexec gw /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf
+ jexec gw /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf
# Confirm that we're getting redirected
atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7"
@@ -304,7 +304,7 @@ captive_body()
atf_check -s exit:1 -x "echo foo | nc -N 198.51.100.2 7"
# Start a server in srv
- jexec srv /usr/sbin/inetd $(atf_get_srcdir)/echo_inetd.conf
+ jexec srv /usr/sbin/inetd -p ${PWD}/echo_inetd.pid $(atf_get_srcdir)/echo_inetd.conf
# And now we can talk to that one.
atf_check -s exit:0 -o match:"^foo$" -x "echo foo | nc -N 198.51.100.2 7"
@@ -362,8 +362,8 @@ captive_long_body()
# ICMP should still work, because we don't redirect it.
atf_check -s exit:0 -o ignore ping -c 1 -t 1 198.51.100.2
- jexec gw /usr/sbin/inetd -p gw.pid $(atf_get_srcdir)/echo_inetd.conf
- jexec srv /usr/sbin/inetd -p srv.pid $(atf_get_srcdir)/daytime_inetd.conf
+ jexec gw /usr/sbin/inetd -p ${PWD}/gw.pid $(atf_get_srcdir)/echo_inetd.conf
+ jexec srv /usr/sbin/inetd -p ${PWD}/srv.pid $(atf_get_srcdir)/daytime_inetd.conf
echo foo | nc -N 198.51.100.2 13
@@ -415,7 +415,7 @@ dummynet_body()
# Sanity check
atf_check -s exit:0 -o ignore ping -i .1 -c 3 -s 1200 192.0.2.2
- jexec alcatraz dnctl pipe 1 config bw 30Byte/s
+ jexec alcatraz dnctl pipe 1 config bw 300Byte/s
jexec alcatraz pfctl -e
pft_set_rules alcatraz \
"ether pass in dnpipe 1"
@@ -430,14 +430,14 @@ dummynet_body()
ping -i .1 -c 5 -s 1200 192.0.2.2
# We should now be hitting the limits and get this packet dropped.
- atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2
+ atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2
# We can now also dummynet outbound traffic!
pft_set_rules alcatraz \
"ether pass out dnpipe 1"
# We should still be hitting the limits and get this packet dropped.
- atf_check -s exit:2 -o ignore ping -c 1 -s 1200 192.0.2.2
+ atf_check -s exit:2 -o ignore ping -c 1 -t 1 -s 1200 192.0.2.2
}
dummynet_cleanup()
@@ -643,7 +643,7 @@ short_pkt_head()
{
atf_set descr 'Test overly short Ethernet packets'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
short_pkt_body()
@@ -686,7 +686,7 @@ bridge_to_head()
{
atf_set descr 'Test bridge-to keyword'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
bridge_to_body()
diff --git a/tests/sys/netpfil/pf/forward.sh b/tests/sys/netpfil/pf/forward.sh
index 31abfad82c13..5d7d48a5dd9a 100644
--- a/tests/sys/netpfil/pf/forward.sh
+++ b/tests/sys/netpfil/pf/forward.sh
@@ -35,7 +35,7 @@ v4_head()
atf_set require.user root
# We need scapy to be installed for out test scripts to work
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v4_body()
@@ -94,7 +94,7 @@ v6_head()
{
atf_set descr 'Basic IPv6 forwarding test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
diff --git a/tests/sys/netpfil/pf/frag-adjhole.py b/tests/sys/netpfil/pf/frag-adjhole.py
new file mode 100644
index 000000000000..99caf66617dd
--- /dev/null
+++ b/tests/sys/netpfil/pf/frag-adjhole.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org>
+
+from fragcommon import *
+
+# |--------|
+# |--------|
+# |-------|
+# |----|
+
+def send(src, dst, send_if, recv_if):
+ pid = os.getpid()
+ eid = pid & 0xffff
+ payload = b"ABCDEFGHIJKLMNOP" * 2
+ packet = sp.IP(src=src, dst=dst)/ \
+ sp.ICMP(type='echo-request', id=eid) / payload
+ frag = []
+ fid = pid & 0xffff
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ flags='MF') / bytes(packet)[20:36])
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=2, flags='MF') / bytes(packet)[36:52])
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=1, flags='MF') / bytes(packet)[28:44])
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=4) / bytes(packet)[52:60])
+ eth=[]
+ for f in frag:
+ eth.append(sp.Ether()/f)
+ if os.fork() == 0:
+ time.sleep(1)
+ sp.sendp(eth, iface=send_if)
+ os._exit(0)
+
+ ans = sp.sniff(iface=recv_if, timeout=3, filter=
+ "ip and src " + dst + " and dst " + src + " and icmp")
+ for a in ans:
+ if a and a.type == sp.ETH_P_IP and \
+ a.payload.proto == 1 and \
+ a.payload.frag == 0 and a.payload.flags == 0 and \
+ sp.icmptypes[a.payload.payload.type] == 'echo-reply':
+ id = a.payload.payload.id
+ print("id=%#x" % (id))
+ if id != eid:
+ print("WRONG ECHO REPLY ID")
+ exit(2)
+ data = a.payload.payload.payload.load
+ print("payload=%s" % (data))
+ if data == payload:
+ exit(0)
+ print("PAYLOAD!=%s" % (payload))
+ exit(1)
+ print("NO ECHO REPLY")
+ exit(2)
+
+if __name__ == '__main__':
+ main(send)
diff --git a/tests/sys/netpfil/pf/frag-overhole.py b/tests/sys/netpfil/pf/frag-overhole.py
new file mode 100644
index 000000000000..91697b6b83c6
--- /dev/null
+++ b/tests/sys/netpfil/pf/frag-overhole.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+#
+# Copyright (c) 2025 Alexander Bluhm <bluhm@openbsd.org>
+
+from fragcommon import *
+
+# index boundary 4096 |
+# |--------------|
+# ....
+# |--------------|
+# |----------|
+# |XXXX----------|
+# |XXXX----|
+# |---|
+
+# this should trigger "frag tail overlap %d" and "frag head overlap %d"
+def send(src, dst, send_if, recv_if):
+ pid = os.getpid()
+ eid = pid & 0xffff
+ payload = b"ABCDEFGHIJKLMNOP"
+ dummy = b"01234567"
+ fragsize = 1024
+ boundary = 4096
+ fragnum = int(boundary / fragsize)
+ packet = sp.IP(src=src, dst=dst)/ \
+ sp.ICMP(type='echo-request', id=eid)/ \
+ ((int((boundary + fragsize) / len(payload)) + 1) * payload)
+ packet_length = len(packet)
+ frag = []
+ fid = pid & 0xffff
+ for i in range(fragnum-1):
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=(i * fragsize)>>3, flags='MF')/
+ bytes(packet)[20 + i * fragsize:20 + (i + 1) * fragsize])
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=(boundary - fragsize) >> 3, flags='MF')/
+ bytes(packet)[20 + boundary - fragsize:20 + boundary - len(dummy)])
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=(boundary - len(dummy)) >> 3, flags='MF')/
+ (dummy+bytes(packet)[20 + boundary:20 + boundary + fragsize]))
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=(boundary - 8 - len(dummy)) >> 3, flags='MF')/
+ (dummy+bytes(packet)[20 + boundary - 8:20 + boundary]))
+ frag.append(sp.IP(src=src, dst=dst, proto=1, id=fid,
+ frag=(boundary + fragsize) >> 3)/bytes(packet)[20 + boundary + fragsize:])
+ eth=[]
+ for f in frag:
+ eth.append(sp.Ether() / f)
+
+ if os.fork() == 0:
+ time.sleep(1)
+ for e in eth:
+ sp.sendp(e, iface=send_if)
+ time.sleep(0.001)
+ os._exit(0)
+
+ ans = sp.sniff(iface=recv_if, timeout=3, filter=
+ "ip and src " + dst + " and dst " + src + " and icmp")
+ for a in ans:
+ if a and a.type == sp.ETH_P_IP and \
+ a.payload.proto == 1 and \
+ a.payload.frag == 0 and \
+ sp.icmptypes[a.payload.payload.type] == 'echo-reply':
+ id = a.payload.payload.id
+ print("id=%#x" % (id))
+ if id != eid:
+ print("WRONG ECHO REPLY ID")
+ exit(2)
+ if a and a.type == sp.ETH_P_IP and \
+ a.payload.proto == 1 and \
+ a.payload.frag > 0 and \
+ a.payload.flags == '':
+ length = (a.payload.frag << 3) + a.payload.len
+ print("len=%d" % (length))
+ if length != packet_length:
+ print("WRONG ECHO REPLY LENGTH")
+ exit(1)
+ exit(0)
+ print("NO ECHO REPLY")
+ exit(1)
+
+if __name__ == '__main__':
+ main(send)
diff --git a/tests/sys/netpfil/pf/frag6.py b/tests/sys/netpfil/pf/frag6.py
index 28b1829d418c..26ae7af7c90c 100644
--- a/tests/sys/netpfil/pf/frag6.py
+++ b/tests/sys/netpfil/pf/frag6.py
@@ -1,25 +1,13 @@
import pytest
import logging
-import threading
-import time
+import random
logging.getLogger("scapy").setLevel(logging.CRITICAL)
+from utils import DelayedSend
from atf_python.sys.net.tools import ToolsHelper
from atf_python.sys.net.vnet import VnetTestTemplate
-class DelayedSend(threading.Thread):
- def __init__(self, packet):
- threading.Thread.__init__(self)
- self._packet = packet
-
- self.start()
-
- def run(self):
- import scapy.all as sp
- time.sleep(1)
- sp.send(self._packet)
-
class TestFrag6(VnetTestTemplate):
- REQUIRED_MODULES = ["pf"]
+ REQUIRED_MODULES = ["pf", "dummymbuf"]
TOPOLOGY = {
"vnet1": {"ifaces": ["if1"]},
"vnet2": {"ifaces": ["if1"]},
@@ -27,18 +15,22 @@ class TestFrag6(VnetTestTemplate):
}
def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
- "scrub fragment reassemble",
+ "scrub fragment reassemble min-ttl 10",
"pass",
"block in inet6 proto icmp6 icmp6-type echoreq",
])
+ ToolsHelper.print_output("/sbin/pfilctl link -i dummymbuf:inet6 inet6")
+ ToolsHelper.print_output("/sbin/sysctl net.dummymbuf.rules=\"inet6 in %s enlarge 3000;\"" % ifname)
def check_ping_reply(self, packet):
print(packet)
return False
@pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
def test_dup_frag_hdr(self):
"Test packets with duplicate fragment headers"
srv_vnet = self.vnet_map["vnet2"]
@@ -58,3 +50,227 @@ class TestFrag6(VnetTestTemplate):
timeout=3)
for p in packets:
assert not p.getlayer(sp.ICMPv6EchoReply)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_overlong(self):
+ "Test overly long fragmented packet"
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ curr = 0
+ pkts = []
+
+ frag_id = random.randint(0,0xffffffff)
+ gran = 1200
+
+ i = 0
+ while curr <= 65535:
+ ipv61 = sp.IPv6(src="2001:db8::1", dst="2001:db8::2")
+ more = True
+ g = gran
+ if curr + gran > 65535:
+ more = False
+ g = 65530 - curr
+ if i == 0:
+ pkt = ipv61 / sp.IPv6ExtHdrHopByHop(options=[sp.PadN(optlen=2), sp.Pad1()]) / \
+ sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g)
+ else:
+ pkt = ipv61 / sp.IPv6ExtHdrFragment(id = frag_id, offset = curr // 8, m = more) / bytes([i] * g)
+ pkts.append(pkt)
+ curr += gran
+ i += 1
+
+ sp.send(pkts, inter = 0.1)
+
+class TestFrag6HopHyHop(VnetTestTemplate):
+ REQUIRED_MODULES = ["pf"]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1", "if2"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]},
+ "if2": {"prefixes6": [("2001:db8:666::1/64", "2001:db8:1::2/64")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
+ ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1")
+ ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::1 00:01:02:03:04:05")
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+ ToolsHelper.pf_rules([
+ "scrub fragment reassemble min-ttl 10",
+ "pass allow-opts",
+ ])
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_hop_by_hop(self):
+ "Verify that we reject non-first hop-by-hop headers"
+ if1 = self.vnet.iface_alias_map["if1"].name
+ if2 = self.vnet.iface_alias_map["if2"].name
+ ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2")
+ ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # A hop-by-hop header is accepted if it's the first header
+ pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+ / sp.IPv6ExtHdrHopByHop() \
+ / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+ pkt.show()
+
+ # Delay the send so the sniffer is running when we transmit.
+ s = DelayedSend(pkt)
+
+ replies = sp.sniff(iface=if2, timeout=3)
+ found = False
+ for p in replies:
+ p.show()
+ ip6 = p.getlayer(sp.IPv6)
+ hbh = p.getlayer(sp.IPv6ExtHdrHopByHop)
+ icmp6 = p.getlayer(sp.ICMPv6EchoRequest)
+
+ if not ip6 or not icmp6:
+ continue
+ assert ip6.src == "2001:db8::1"
+ assert ip6.dst == "2001:db8:1::1"
+ assert hbh
+ assert icmp6
+ found = True
+ assert found
+
+ # A hop-by-hop header causes the packet to be dropped if it's not the
+ # first extension header
+ pkt = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+ / sp.IPv6ExtHdrFragment(offset=0, m=0) \
+ / sp.IPv6ExtHdrHopByHop() \
+ / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+ pkt2 = sp.IPv6(src="2001:db8::1", dst="2001:db8:1::1") \
+ / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 30))
+
+ # Delay the send so the sniffer is running when we transmit.
+ ToolsHelper.print_output("/sbin/ping6 -c 1 2001:db8:1::2")
+
+ s = DelayedSend([ pkt2, pkt ])
+ replies = sp.sniff(iface=if2, timeout=10)
+ found = False
+ for p in replies:
+ # Expect to find the packet without the hop-by-hop header, not the
+ # one with
+ p.show()
+ ip6 = p.getlayer(sp.IPv6)
+ hbh = p.getlayer(sp.IPv6ExtHdrHopByHop)
+ icmp6 = p.getlayer(sp.ICMPv6EchoRequest)
+
+ if not ip6 or not icmp6:
+ continue
+ assert ip6.src == "2001:db8::1"
+ assert ip6.dst == "2001:db8:1::1"
+ assert not hbh
+ assert icmp6
+ found = True
+ assert found
+
+class TestFrag6_Overlap(VnetTestTemplate):
+ REQUIRED_MODULES = ["pf"]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1"]},
+ "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+ ToolsHelper.pf_rules([
+ "scrub fragment reassemble",
+ "pass",
+ ])
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_overlap(self):
+ "Ensure we discard packets with overlapping fragments"
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ packet = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \
+ / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f00f') * 90))
+ frags = sp.fragment6(packet, 128)
+ assert len(frags) == 3
+
+ f = frags[0].getlayer(sp.IPv6ExtHdrFragment)
+ # Fragment with overlap
+ overlap = sp.IPv6(src="2001:db8::1", dst="2001:db8::2") \
+ / sp.IPv6ExtHdrFragment(offset = 4, m = 1, id = f.id, nh = f.nh) \
+ / sp.raw(bytes.fromhex('f00f') * 4)
+ frags = [ frags[0], frags[1], overlap, frags[2] ]
+
+ # Delay the send so the sniffer is running when we transmit.
+ s = DelayedSend(frags)
+
+ packets = sp.sniff(iface=self.vnet.iface_alias_map["if1"].name,
+ timeout=3)
+ for p in packets:
+ p.show()
+ assert not p.getlayer(sp.ICMPv6EchoReply)
+
+class TestFrag6_RouteTo(VnetTestTemplate):
+ REQUIRED_MODULES = ["pf"]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "vnet3": {"ifaces": ["if2"]},
+ "if1": {"prefixes6": [("2001:db8::1/64", "2001:db8::2/64")]},
+ "if2": {"prefixes6": [("2001:db8:1::1/64", "2001:db8:1::2/64")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ if2name = vnet.iface_alias_map["if2"].name
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+ ToolsHelper.pf_rules([
+ "scrub fragment reassemble",
+ "pass in route-to (%s 2001:db8:1::2) from 2001:db8::1 to 2001:db8:666::1" % if2name,
+ ])
+
+ ToolsHelper.print_output("/sbin/ifconfig %s mtu 1300" % if2name)
+ ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1")
+
+ def vnet3_handler(self, vnet):
+ pass
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_too_big(self):
+ ToolsHelper.print_output("/sbin/route add -6 default 2001:db8::2")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ pkt = sp.IPv6(dst="2001:db8:666::1") \
+ / sp.ICMPv6EchoRequest(data=sp.raw(bytes.fromhex('f0') * 3000))
+ frags = sp.fragment6(pkt, 1320)
+
+ reply = sp.sr1(frags, timeout=3)
+ if reply:
+ reply.show()
+
+ assert reply
+
+ ip6 = reply.getlayer(sp.IPv6)
+ icmp6 = reply.getlayer(sp.ICMPv6PacketTooBig)
+ err_ip6 = reply.getlayer(sp.IPerror6)
+
+ assert ip6
+ assert ip6.src == "2001:db8::2"
+ assert ip6.dst == "2001:db8::1"
+ assert icmp6
+ assert icmp6.mtu == 1300
+ assert err_ip6
+ assert err_ip6.src == "2001:db8::1"
+ assert err_ip6.dst == "2001:db8:666::1"
diff --git a/tests/sys/netpfil/pf/fragmentation_compat.sh b/tests/sys/netpfil/pf/fragmentation_compat.sh
index 21ce6b734ea1..1f4550ebd69e 100644
--- a/tests/sys/netpfil/pf/fragmentation_compat.sh
+++ b/tests/sys/netpfil/pf/fragmentation_compat.sh
@@ -74,7 +74,7 @@ v6_head()
{
atf_set descr 'IPv6 fragmentation test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
@@ -230,7 +230,7 @@ overreplace_head()
{
atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overreplace_body()
@@ -248,7 +248,7 @@ overindex_head()
{
atf_set descr 'ping fragment that overlaps the first fragment at index boundary'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overindex_body()
@@ -266,7 +266,7 @@ overlimit_head()
{
atf_set descr 'ping fragment at index boundary that cannot be requeued'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overlimit_body()
@@ -326,53 +326,6 @@ reassemble_cleanup()
pft_cleanup
}
-atf_test_case "no_df" "cleanup"
-no_df_head()
-{
- atf_set descr 'Test removing of DF flag'
- atf_set require.user root
-}
-
-no_df_body()
-{
- setup_router_server_ipv4
-
- ifconfig ${epair_tester}a mtu 9000
- jexec router ifconfig ${epair_tester}b mtu 9000
- jexec router ifconfig ${epair_server}a mtu 1500
- jexec server ifconfig ${epair_server}b mtu 1500
-
- # Sanity check.
- ping_server_check_reply exit:0 --ping-type=icmp
-
- pft_set_rules router \
- "scrub fragment reassemble" \
- "pass out" \
- "block in" \
- "pass in inet proto icmp all icmp-type echoreq"
-
- # Ping with normal, fragmentable packets.
- ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000
-
- # Ping with non-fragmentable packets, this will fail.
- ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF
-
- pft_set_rules router \
- "scrub any reassemble" \
- "pass out" \
- "block in" \
- "pass in inet proto icmp all icmp-type echoreq"
-
- # Ping with non-fragmentable packets again.
- # This time pf will strip the DF flag.
- ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF
-}
-
-no_df_cleanup()
-{
- pft_cleanup
-}
-
atf_init_test_cases()
{
atf_add_test_case "too_many_fragments"
diff --git a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh
index fb5c15ac2ff8..7cab89f5debb 100644
--- a/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh
+++ b/tests/sys/netpfil/pf/fragmentation_no_reassembly.sh
@@ -32,7 +32,7 @@ match_full_v4_head()
{
atf_set descr 'Matching non-fragmented IPv4 packets'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
match_full_v4_body()
@@ -63,7 +63,7 @@ match_fragment_v4_head()
{
atf_set descr 'Matching fragmented IPv4 packets'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
match_fragment_v4_body()
@@ -93,7 +93,7 @@ compat_override_v4_head()
{
atf_set descr 'Scrub rules override "set reassemble" for IPv4'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
compat_override_v4_body()
diff --git a/tests/sys/netpfil/pf/fragmentation_pass.sh b/tests/sys/netpfil/pf/fragmentation_pass.sh
index d505accba5f2..5deaba18301d 100644
--- a/tests/sys/netpfil/pf/fragmentation_pass.sh
+++ b/tests/sys/netpfil/pf/fragmentation_pass.sh
@@ -77,7 +77,7 @@ v6_head()
{
atf_set descr 'IPv6 fragmentation test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
@@ -155,6 +155,75 @@ v6_cleanup()
pft_cleanup
}
+atf_test_case "v6_route_to" "cleanup"
+v6_route_to_head()
+{
+ atf_set descr 'Test IPv6 reassembly combined with route-to'
+ atf_set require.user root
+}
+
+v6_route_to_body()
+{
+ pft_init
+
+ epair_send=$(vnet_mkepair)
+ epair_link=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair_send}b ${epair_link}a
+ vnet_mkjail singsing ${epair_link}b
+
+ ifconfig ${epair_send}a inet6 2001:db8:42::1/64 no_dad up
+
+ jexec alcatraz ifconfig ${epair_send}b inet6 2001:db8:42::2/64 no_dad up
+ jexec alcatraz ifconfig ${epair_link}a inet6 2001:db8:43::2/64 no_dad up
+ jexec alcatraz sysctl net.inet6.ip6.forwarding=1
+
+ jexec singsing ifconfig ${epair_link}b inet6 2001:db8:43::3/64 no_dad up
+ jexec singsing route add -6 2001:db8:42::/64 2001:db8:43::2
+ route add -6 2001:db8:43::/64 2001:db8:42::2
+
+ jexec alcatraz ifconfig ${epair_send}b inet6 -ifdisabled
+ jexec alcatraz ifconfig ${epair_link}a inet6 -ifdisabled
+ jexec singsing ifconfig ${epair_link}b inet6 -ifdisabled
+ ifconfig ${epair_send}a inet6 -ifdisabled
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "set reassemble yes" \
+ "pass" \
+ "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state"
+
+ # Forwarding test
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 2001:db8:43::3
+
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 -s 4500 2001:db8:43::3
+
+ atf_check -s exit:0 -o ignore\
+ ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3
+
+ # Now test this without fragmentation
+ pft_set_rules alcatraz \
+ "set reassemble no" \
+ "pass" \
+ "pass in route-to (${epair_link}a 2001:db8:43::3) inet6 proto icmp6 from any to 2001:db8:43::3 keep state"
+
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 2001:db8:43::3
+
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 -s 4500 2001:db8:43::3
+
+ atf_check -s exit:0 -o ignore\
+ ping -6 -c 1 -b 70000 -s 65000 2001:db8:43::3
+}
+
+v6_route_to_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "mtu_diff" "cleanup"
mtu_diff_head()
{
@@ -236,7 +305,7 @@ overreplace_head()
{
atf_set descr 'ping fragment that overlaps fragment at index boundary and replace it'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overreplace_body()
@@ -254,7 +323,7 @@ overindex_head()
{
atf_set descr 'ping fragment that overlaps the first fragment at index boundary'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overindex_body()
@@ -272,7 +341,7 @@ overlimit_head()
{
atf_set descr 'ping fragment at index boundary that cannot be requeued'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
overlimit_body()
@@ -285,6 +354,42 @@ overlimit_cleanup()
pft_cleanup
}
+atf_test_case "overhole" "cleanup"
+overhole_head()
+{
+ atf_set descr 'ping fragment at index boundary which modifies pf hole counter'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+overhole_body()
+{
+ frag_common overhole
+}
+
+overhole_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "adjhole" "cleanup"
+adjhole_head()
+{
+ atf_set descr 'overlapping ping fragments which modifies pf hole counter'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+adjhole_body()
+{
+ frag_common adjhole
+}
+
+adjhole_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "reassemble" "cleanup"
reassemble_head()
{
@@ -338,61 +443,7 @@ no_df_head()
{
atf_set descr 'Test removing of DF flag'
atf_set require.user root
-}
-
-no_df_body()
-{
- setup_router_server_ipv4
-
- ifconfig ${epair_tester}a mtu 9000
- jexec router ifconfig ${epair_tester}b mtu 9000
- jexec router ifconfig ${epair_server}a mtu 1500
- jexec server ifconfig ${epair_server}b mtu 1500
-
- # Sanity check.
- ping_server_check_reply exit:0 --ping-type=icmp
-
- pft_set_rules router \
- "set reassemble no" \
- "pass out" \
- "block in" \
- "pass in inet proto icmp all icmp-type echoreq"
-
- # Ping with normal, fragmentable packets.
- ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000
-
- pft_set_rules router \
- "set reassemble yes" \
- "pass out" \
- "block in" \
- "pass in inet proto icmp all icmp-type echoreq"
-
- # Ping with normal, fragmentable packets.
- ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000
-
- # Ping with non-fragmentable packets.
- ping_server_check_reply exit:1 --ping-type=icmp --send-length=2000 --send-flags DF
-
- pft_set_rules router \
- "set reassemble yes no-df" \
- "pass out" \
- "block in" \
- "pass in inet proto icmp all icmp-type echoreq"
-
- # Ping with non-fragmentable packets again.
- # This time pf will strip the DF flag.
- ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF
-}
-no_df_cleanup()
-{
- pft_cleanup
-}
-
-atf_test_case "no_df" "cleanup"
-no_df_head()
-{
- atf_set descr 'Test removing of DF flag'
- atf_set require.user root
+ atf_set require.progs python3 scapy
}
no_df_body()
@@ -420,6 +471,7 @@ no_df_body()
# getting properly forwarded.
ping_server_check_reply exit:0 --ping-type=icmp --send-length=2000 --send-flags DF
}
+
no_df_cleanup()
{
pft_cleanup
@@ -430,6 +482,7 @@ reassemble_slowpath_head()
{
atf_set descr 'Test reassembly on the slow path'
atf_set require.user root
+ atf_set require.progs python3 scapy
}
reassemble_slowpath_body()
@@ -553,17 +606,63 @@ dummynet_nat_cleanup()
pft_cleanup
}
+atf_test_case "dummynet_fragmented" "cleanup"
+dummynet_fragmented_head()
+{
+ atf_set descr 'Test dummynet on NATed fragmented traffic'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+dummynet_fragmented_body()
+{
+ pft_init
+ dummynet_init
+
+ # No test for IPv6. IPv6 fragment reassembly can't be disabled.
+ setup_router_dummy_ipv4
+
+ jexec router dnctl pipe 1 config delay 1
+
+ pft_set_rules router \
+ "set reassemble no" \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in on ${epair_tester}b inet proto udp dnpipe (1, 1)" \
+ "pass out on ${epair_server}a inet proto udp" \
+
+ ping_dummy_check_request exit:0 --ping-type=udp --send-length=10000 --send-frag-length=1280
+
+ rules=$(mktemp) || exit 1
+ jexec router pfctl -qvsr | normalize_pfctl_s > $rules
+
+ # Count that fragmented packets have hit the rule only once and that
+ # they have not created states. There is no stateful firewall support
+ # for fragmented packets.
+ grep -qE 'pass in on epair0b inet proto udp all keep state dnpipe\(1, 1\) .* Packets: 8 Bytes: 10168 States: 0 ' $rules ||
+ atf_fail "Fragmented packets not counted correctly"
+}
+
+dummynet_fragmented_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "too_many_fragments"
atf_add_test_case "v6"
+ atf_add_test_case "v6_route_to"
atf_add_test_case "mtu_diff"
atf_add_test_case "overreplace"
atf_add_test_case "overindex"
atf_add_test_case "overlimit"
+ atf_add_test_case "overhole"
+ atf_add_test_case "adjhole"
atf_add_test_case "reassemble"
atf_add_test_case "no_df"
atf_add_test_case "reassemble_slowpath"
atf_add_test_case "dummynet"
atf_add_test_case "dummynet_nat"
+ atf_add_test_case "dummynet_fragmented"
}
diff --git a/tests/sys/netpfil/pf/get_state.sh b/tests/sys/netpfil/pf/get_state.sh
index 35adf64f6b11..eb2bc854c800 100644
--- a/tests/sys/netpfil/pf/get_state.sh
+++ b/tests/sys/netpfil/pf/get_state.sh
@@ -33,7 +33,7 @@ many_head()
{
atf_set descr 'Test retrieving many states'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
many_body()
diff --git a/tests/sys/netpfil/pf/header.py b/tests/sys/netpfil/pf/header.py
new file mode 100644
index 000000000000..6832cfe6d42b
--- /dev/null
+++ b/tests/sys/netpfil/pf/header.py
@@ -0,0 +1,217 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import pytest
+import re
+from utils import DelayedSend
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+class TestHeader(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1", "if2"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
+ "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
+ ToolsHelper.print_output("/usr/sbin/arp -s 198.51.100.3 00:01:02:03:04:05")
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+ ToolsHelper.pf_rules([
+ "pass",
+ ])
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_too_many(self):
+ "Verify that we drop packets with silly numbers of headers."
+
+ sendif = self.vnet.iface_alias_map["if1"].name
+ recvif = self.vnet.iface_alias_map["if2"].name
+ gw_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
+ gw_mac = re.sub("0a$", "0b", gw_mac)
+
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Sanity check, ensure we get replies to normal ping
+ pkt = sp.Ether(dst=gw_mac) \
+ / sp.IP(dst="198.51.100.3") \
+ / sp.ICMP(type='echo-request')
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+
+ found = False
+ for r in reply:
+ r.show()
+
+ icmp = r.getlayer(sp.ICMP)
+ if not icmp:
+ continue
+ assert icmp.type == 8 # 'echo-request'
+ found = True
+ assert found
+
+ # Up to 19 AH headers will pass
+ pkt = sp.Ether(dst=gw_mac) \
+ / sp.IP(dst="198.51.100.3")
+ for i in range(0, 18):
+ pkt = pkt / sp.AH(nh=51, payloadlen=1)
+ pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request')
+
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+ found = False
+ for r in reply:
+ r.show()
+
+ ah = r.getlayer(sp.AH)
+ if not ah:
+ continue
+ found = True
+ assert found
+
+ # But more will get dropped
+ pkt = sp.Ether(dst=gw_mac) \
+ / sp.IP(dst="198.51.100.3")
+ for i in range(0, 19):
+ pkt = pkt / sp.AH(nh=51, payloadlen=1)
+ pkt = pkt / sp.AH(nh=1, payloadlen=1) / sp.ICMP(type='echo-request')
+
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+
+ found = False
+ for r in reply:
+ r.show()
+
+ ah = r.getlayer(sp.AH)
+ if not ah:
+ continue
+ found = True
+ assert not found
+
+class TestHeader6(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ SKIP_MODULES = [ "ipfilter" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1", "if2"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]},
+ "if2": {"prefixes6": [("2001:db8:1::2/64", "2001:db8:1::1/64")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/sysctl net.inet6.ip6.forwarding=1")
+ ToolsHelper.print_output("/usr/sbin/ndp -s 2001:db8:1::3 00:01:02:03:04:05")
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+ ToolsHelper.pf_rules([
+ "pass",
+ ])
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_too_many(self):
+ "Verify that we drop packets with silly numbers of headers."
+ ToolsHelper.print_output("/sbin/ifconfig")
+
+ sendif = self.vnet.iface_alias_map["if1"].name
+ recvif = self.vnet.iface_alias_map["if2"].name
+ our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
+ gw_mac = re.sub("0a$", "0b", our_mac)
+
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Sanity check, ensure we get replies to normal ping
+ pkt = sp.Ether(src=our_mac, dst=gw_mac) \
+ / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3") \
+ / sp.ICMPv6EchoRequest()
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+
+ found = False
+ for r in reply:
+ r.show()
+
+ icmp = r.getlayer(sp.ICMPv6EchoRequest)
+ if not icmp:
+ continue
+ found = True
+ assert found
+
+ # Up to 19 AH headers will pass
+ pkt = sp.Ether(src=our_mac, dst=gw_mac) \
+ / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3")
+ for i in range(0, 18):
+ pkt = pkt / sp.AH(nh=51, payloadlen=1)
+ pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest()
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+
+ found = False
+ for r in reply:
+ r.show()
+
+ ah = r.getlayer(sp.AH)
+ if not ah:
+ continue
+ found = True
+ assert found
+
+ # But more will get dropped
+ pkt = sp.Ether(src=our_mac, dst=gw_mac) \
+ / sp.IPv6(src="2001:db8::2", dst="2001:db8:1::3")
+ for i in range(0, 19):
+ pkt = pkt / sp.AH(nh=51, payloadlen=1)
+ pkt = pkt / sp.AH(nh=58, payloadlen=1) / sp.ICMPv6EchoRequest()
+ s = DelayedSend(pkt, sendif)
+ reply = sp.sniff(iface=recvif, timeout=3)
+ print(reply)
+
+ found = False
+ for r in reply:
+ r.show()
+
+ ah = r.getlayer(sp.AH)
+ if not ah:
+ continue
+ found = True
+ assert not found
diff --git a/tests/sys/netpfil/pf/icmp.py b/tests/sys/netpfil/pf/icmp.py
new file mode 100644
index 000000000000..83096886691e
--- /dev/null
+++ b/tests/sys/netpfil/pf/icmp.py
@@ -0,0 +1,254 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import pytest
+import subprocess
+import re
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+def check(cmd):
+ ps = subprocess.Popen(cmd, shell=True)
+ ret = ps.wait()
+ if ret != 0:
+ raise Exception("Command %s returned %d" % (cmd, ret))
+
+class TestICMP(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "vnet3": {"ifaces": ["if2"]},
+ "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
+ "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
+ if2name = vnet.iface_alias_map["if2"].name
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "set reassemble yes",
+ "set state-policy if-bound",
+ "block",
+ "pass inet proto icmp icmp-type echoreq",
+ ])
+
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+
+ ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % if2name)
+
+ def vnet3_handler(self, vnet):
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ ifname = vnet.iface_alias_map["if2"].name
+ ToolsHelper.print_output("/sbin/route add default 198.51.100.1")
+ ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname)
+ ToolsHelper.print_output("/sbin/ifconfig %s mtu 1492" % ifname)
+
+ def checkfn(packet):
+ icmp = packet.getlayer(sp.ICMP)
+ if not icmp:
+ return False
+
+ if icmp.type != 3:
+ return False
+
+ packet.show()
+ return True
+
+ sp.sniff(iface=ifname, stop_filter=checkfn)
+ vnet.pipe.send("Got ICMP destination unreachable packet")
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_inner_match(self):
+ vnet = self.vnet_map["vnet1"]
+ dst_vnet = self.vnet_map["vnet3"]
+ sendif = vnet.iface_alias_map["if1"].name
+
+ our_mac = ToolsHelper.get_output("/sbin/ifconfig %s ether | awk '/ether/ { print $2; }'" % sendif)
+ dst_mac = re.sub("0a$", "0b", our_mac)
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
+
+ # Sanity check
+ check("/sbin/ping -c 1 192.0.2.1")
+ check("/sbin/ping -c 1 198.51.100.1")
+ check("/sbin/ping -c 2 198.51.100.3")
+
+ # Establish a state
+ pkt = sp.Ether(src=our_mac, dst=dst_mac) \
+ / sp.IP(src="192.0.2.2", dst="198.51.100.2") \
+ / sp.ICMP(type='echo-request') \
+ / "PAYLOAD"
+ sp.sendp(pkt, sendif, verbose=False)
+
+ # Now try to pass an ICMP error message piggy-backing on that state, but
+ # use a different source address
+ pkt = sp.Ether(src=our_mac, dst=dst_mac) \
+ / sp.IP(src="192.0.2.2", dst="198.51.100.3") \
+ / sp.ICMP(type='dest-unreach') \
+ / sp.IP(src="198.51.100.2", dst="192.0.2.2") \
+ / sp.ICMP(type='echo-reply')
+ sp.sendp(pkt, sendif, verbose=False)
+
+ try:
+ rcvd = self.wait_object(dst_vnet.pipe, timeout=1)
+ if rcvd:
+ raise Exception(rcvd)
+ except TimeoutError as e:
+ # We expect the timeout here. It means we didn't get the destination
+ # unreachable packet in vnet3
+ pass
+
+ def check_icmp_echo(self, sp, payload_size):
+ packet = sp.IP(dst="198.51.100.2", flags="DF") \
+ / sp.ICMP(type='echo-request') \
+ / sp.raw(bytes.fromhex('f0') * payload_size)
+
+ p = sp.sr1(packet, iface=self.vnet.iface_alias_map["if1"].name,
+ timeout=3)
+ p.show()
+
+ ip = p.getlayer(sp.IP)
+ icmp = p.getlayer(sp.ICMP)
+ assert ip
+ assert icmp
+
+ if payload_size > 1464:
+ # Expect ICMP destination unreachable, fragmentation needed
+ assert ip.src == "192.0.2.1"
+ assert ip.dst == "192.0.2.2"
+ assert icmp.type == 3 # dest-unreach
+ assert icmp.code == 4
+ assert icmp.nexthopmtu == 1492
+ else:
+ # Expect echo reply
+ assert ip.src == "198.51.100.2"
+ assert ip.dst == "192.0.2.2"
+ assert icmp.type == 0 # "echo-reply"
+ assert icmp.code == 0
+
+ return
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_fragmentation_needed(self):
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
+
+ ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2")
+ ToolsHelper.print_output("/sbin/ping -c 1 -D -s 1472 198.51.100.2")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ self.check_icmp_echo(sp, 128)
+ self.check_icmp_echo(sp, 1464)
+ self.check_icmp_echo(sp, 1468)
+
+class TestICMP_NAT(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "vnet3": {"ifaces": ["if2"]},
+ "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
+ "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
+ if2name = vnet.iface_alias_map["if2"].name
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "set reassemble yes",
+ "set state-policy if-bound",
+ "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (if2name, if2name),
+ "block",
+ "pass inet proto icmp icmp-type echoreq",
+ ])
+
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+
+ def vnet3_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if2"].name
+ ToolsHelper.print_output("/sbin/ifconfig %s inet alias 198.51.100.3/24" % ifname)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_id_conflict(self):
+ """
+ Test ICMP echo requests with the same ID from different clients.
+ Windows does this, and it can confuse pf.
+ """
+ ifname = self.vnet.iface_alias_map["if1"].name
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
+ ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.3/24" % ifname)
+
+ ToolsHelper.print_output("/sbin/ping -c 1 192.0.2.1")
+ ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.1")
+ ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.2")
+ ToolsHelper.print_output("/sbin/ping -c 1 198.51.100.3")
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Address one
+ packet = sp.IP(src="192.0.2.2", dst="198.51.100.2", flags="DF") \
+ / sp.ICMP(type='echo-request', id=42) \
+ / sp.raw(bytes.fromhex('f0') * 16)
+
+ p = sp.sr1(packet, timeout=3)
+ p.show()
+ ip = p.getlayer(sp.IP)
+ icmp = p.getlayer(sp.ICMP)
+ assert ip
+ assert icmp
+ assert ip.dst == "192.0.2.2"
+ assert icmp.id == 42
+
+ # Address one
+ packet = sp.IP(src="192.0.2.3", dst="198.51.100.2", flags="DF") \
+ / sp.ICMP(type='echo-request', id=42) \
+ / sp.raw(bytes.fromhex('f0') * 16)
+
+ p = sp.sr1(packet, timeout=3)
+ p.show()
+ ip = p.getlayer(sp.IP)
+ icmp = p.getlayer(sp.ICMP)
+ assert ip
+ assert icmp
+ assert ip.dst == "192.0.2.3"
+ assert icmp.id == 42
diff --git a/tests/sys/netpfil/pf/icmp.sh b/tests/sys/netpfil/pf/icmp.sh
index 72b531b08c51..279e3c7a29d5 100644
--- a/tests/sys/netpfil/pf/icmp.sh
+++ b/tests/sys/netpfil/pf/icmp.sh
@@ -33,7 +33,7 @@ cve_2019_5598_head()
{
atf_set descr 'Test CVE-2019-5598'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
cve_2019_5598_body()
@@ -71,7 +71,74 @@ cve_2019_5598_cleanup()
pft_cleanup
}
+atf_test_case "ttl_exceeded" "cleanup"
+ttl_exceeded_head()
+{
+ atf_set descr 'Test that we correctly translate TTL exceeded back'
+ atf_set require.user root
+}
+
+ttl_exceeded_body()
+{
+ pft_init
+
+ epair_srv=$(vnet_mkepair)
+ epair_int=$(vnet_mkepair)
+ epair_cl=$(vnet_mkepair)
+
+ vnet_mkjail srv ${epair_srv}a
+ jexec srv ifconfig ${epair_srv}a 192.0.2.1/24 up
+ jexec srv route add default 192.0.2.2
+
+ vnet_mkjail int ${epair_srv}b ${epair_int}a
+ jexec int sysctl net.inet.ip.forwarding=1
+ jexec int ifconfig ${epair_srv}b 192.0.2.2/24 up
+ jexec int ifconfig ${epair_int}a 203.0.113.2/24 up
+
+ vnet_mkjail nat ${epair_int}b ${epair_cl}b
+ jexec nat ifconfig ${epair_int}b 203.0.113.1/24 up
+ jexec nat ifconfig ${epair_cl}b 198.51.100.2/24 up
+ jexec nat sysctl net.inet.ip.forwarding=1
+ jexec nat route add default 203.0.113.2
+
+ vnet_mkjail cl ${epair_cl}a
+ jexec cl ifconfig ${epair_cl}a 198.51.100.1/24 up
+ jexec cl route add default 198.51.100.2
+
+ jexec nat pfctl -e
+ pft_set_rules nat \
+ "nat on ${epair_int}b from 198.51.100.0/24 -> (${epair_int}b)" \
+ "block" \
+ "pass inet proto udp" \
+ "pass inet proto icmp icmp-type { echoreq }"
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 198.51.100.2
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 203.0.113.1
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 203.0.113.2
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 192.0.2.1
+
+ echo "UDP"
+ atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \
+ jexec cl traceroute 192.0.2.1
+ jexec nat pfctl -Fs
+
+ echo "ICMP"
+ atf_check -s exit:0 -e ignore -o match:".*203.0.113.2.*" \
+ jexec cl traceroute -I 192.0.2.1
+}
+
+ttl_exceeded_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "cve_2019_5598"
+ atf_add_test_case "ttl_exceeded"
}
diff --git a/tests/sys/netpfil/pf/icmp6.sh b/tests/sys/netpfil/pf/icmp6.sh
new file mode 100644
index 000000000000..c55af906e3a6
--- /dev/null
+++ b/tests/sys/netpfil/pf/icmp6.sh
@@ -0,0 +1,204 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+common_dir=$(atf_get_srcdir)/../common
+
+atf_test_case "zero_id" "cleanup"
+zero_id_head()
+{
+ atf_set descr 'Test ICMPv6 echo with ID 0 keep being blocked'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+zero_id_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 2001:db8::1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "set block-policy drop" \
+ "antispoof quick for { egress ${epair}b }" \
+ "block all" \
+ "pass out" \
+ "pass in quick inet6 proto IPV6-ICMP icmp6-type 135" \
+ "pass in quick inet6 proto IPV6-ICMP icmp6-type 136" \
+ "pass out quick inet6 proto IPV6 from self to any"
+
+ # Now we can't ping
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 2001:db8::1
+
+ # Force neighbour discovery
+ ndp -d 2001:db8::1
+
+ # Verify that we don't confuse echo request with ID 0 for neighbour discovery
+ atf_check -s exit:1 -o ignore \
+ ${common_dir}/pft_ping.py \
+ --sendif ${epair}a \
+ --to 2001:db8::1 \
+ --replyif ${epair}a
+
+ jexec alcatraz pfctl -ss -vv
+ jexec alcatraz pfctl -sr -vv
+}
+
+zero_id_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "ttl_exceeded" "cleanup"
+ttl_exceeded_head()
+{
+ atf_set descr 'Test that we correctly translate TTL exceeded back'
+ atf_set require.user root
+}
+
+ttl_exceeded_body()
+{
+ pft_init
+
+ epair_srv=$(vnet_mkepair)
+ epair_int=$(vnet_mkepair)
+ epair_cl=$(vnet_mkepair)
+
+ vnet_mkjail srv ${epair_srv}a
+ jexec srv ifconfig ${epair_srv}a inet6 2001:db8:1::1/64 no_dad up
+ jexec srv route add -6 default 2001:db8:1::2
+
+ vnet_mkjail int ${epair_srv}b ${epair_int}a
+ jexec int sysctl net.inet6.ip6.forwarding=1
+ jexec int ifconfig ${epair_srv}b inet6 2001:db8:1::2/64 no_dad up
+ jexec int ifconfig ${epair_int}a inet6 2001:db8:2::2/64 no_dad up
+
+ vnet_mkjail nat ${epair_int}b ${epair_cl}b
+ jexec nat ifconfig ${epair_int}b inet6 2001:db8:2::1 no_dad up
+ jexec nat ifconfig ${epair_cl}b inet6 2001:db8:3::2/64 no_dad up
+ jexec nat sysctl net.inet6.ip6.forwarding=1
+ jexec nat route add -6 default 2001:db8:2::2
+
+ vnet_mkjail cl ${epair_cl}a
+ jexec cl ifconfig ${epair_cl}a inet6 2001:db8:3::1/64 no_dad up
+ jexec cl route add -6 default 2001:db8:3::2
+
+ jexec nat pfctl -e
+ pft_set_rules nat \
+ "nat on ${epair_int}b from 2001:db8:3::/64 -> (${epair_int}b:0)" \
+ "block" \
+ "pass inet6 proto udp" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv, echoreq }"
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 2001:db8:3::2
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 2001:db8:2::1
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 2001:db8:2::2
+ atf_check -s exit:0 -o ignore \
+ jexec cl ping -c 1 2001:db8:1::1
+
+ echo "UDP"
+ atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \
+ jexec cl traceroute6 2001:db8:1::1
+ jexec nat pfctl -Fs
+
+ echo "ICMP"
+ atf_check -s exit:0 -e ignore -o match:".*2001:db8:2::2.*" \
+ jexec cl traceroute6 -I 2001:db8:1::1
+}
+
+ttl_exceeded_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "repeat" "cleanup"
+repeat_head()
+{
+ atf_set descr 'Ensure that repeated NDs work'
+ atf_set require.user root
+ atf_set require.progs ndisc6
+}
+
+repeat_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 2001:db8::1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block all" \
+ "pass quick inet6 proto ipv6-icmp all icmp6-type neighbrsol keep state (if-bound) ridentifier 1000000107"
+
+ jexec alcatraz pfctl -x loud
+ ndisc6 -m -n -r 1 2001:db8::1 ${epair}a
+ jexec alcatraz pfctl -ss -vv
+
+ atf_check -s exit:0 -o ignore \
+ ndisc6 -m -n -r 1 2001:db8::1 ${epair}a
+ jexec alcatraz pfctl -ss -vv
+ atf_check -s exit:0 -o ignore \
+ ndisc6 -m -n -r 1 2001:db8::1 ${epair}a
+ jexec alcatraz pfctl -ss -vv
+ atf_check -s exit:0 -o ignore \
+ ndisc6 -m -n -r 1 2001:db8::1 ${epair}a
+ jexec alcatraz pfctl -ss -vv
+}
+
+repeat_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "zero_id"
+ atf_add_test_case "ttl_exceeded"
+ atf_add_test_case "repeat"
+}
diff --git a/tests/sys/netpfil/pf/if_enc.sh b/tests/sys/netpfil/pf/if_enc.sh
new file mode 100644
index 000000000000..40090b175470
--- /dev/null
+++ b/tests/sys/netpfil/pf/if_enc.sh
@@ -0,0 +1,178 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+#
+# The following network is used as a base for testing.
+#
+#
+# ${awan}b |----------| ${bwan}b
+# 2.0.0.1 | host wan | 3.0.0.1
+# .---->| Internet |<----.
+# A WAN | |----------| | B WAN
+# | |
+# Office A side | | Office B side
+# | ${awan}a ${bwan}a |
+# v 2.0.0.22 3.0.0.33 v
+# ${alan}b |----------| |----------| ${blan}b
+# 1.0.0.1 | host agw | | host bgw | 4.0.0.1
+# .----------->| gateway | < IPsec > | gateway |<-----------.
+# | A LAN |----------| tunnel |----------| B LAN |
+# | |
+# | |
+# | ${alan}a ${blan}a |
+# v 1.0.0.11 4.0.0.44 v
+# |----------| |----------|
+# | host a | | host b |
+# | client | | client |
+# |----------| |----------|
+#
+#
+# There is routing between office A clients and office B ones. The traffic is
+# encrypted, i.e. host wan should see IPsec flow (ESP packets).
+#
+
+ipsec_init()
+{
+ if ! sysctl -q kern.features.ipsec >/dev/null ; then
+ atf_skip "This test requires ipsec"
+ fi
+}
+
+if_enc_init()
+{
+ ipsec_init
+ if ! kldstat -q -m if_enc; then
+ atf_skip "This test requires if_enc"
+ fi
+}
+
+build_test_network()
+{
+ alan=$(vnet_mkepair)
+ awan=$(vnet_mkepair)
+ bwan=$(vnet_mkepair)
+ blan=$(vnet_mkepair)
+
+ # host a
+ vnet_mkjail a ${alan}a
+ jexec a ifconfig ${alan}a 1.0.0.11/24 up
+ jexec a route add default 1.0.0.1
+
+ # host agw
+ vnet_mkjail agw ${alan}b ${awan}a
+ jexec agw ifconfig ${alan}b 1.0.0.1/24 up
+ jexec agw ifconfig ${awan}a 2.0.0.22/24 up
+ jexec agw route add default 2.0.0.1
+ jexec agw sysctl net.inet.ip.forwarding=1
+
+ # host wan
+ vnet_mkjail wan ${awan}b ${bwan}b
+ jexec wan ifconfig ${awan}b 2.0.0.1/24 up
+ jexec wan ifconfig ${bwan}b 3.0.0.1/24 up
+ jexec wan sysctl net.inet.ip.forwarding=1
+
+ # host bgw
+ vnet_mkjail bgw ${bwan}a ${blan}b
+ jexec bgw ifconfig ${bwan}a 3.0.0.33/24 up
+ jexec bgw ifconfig ${blan}b 4.0.0.1/24 up
+ jexec bgw route add default 3.0.0.1
+ jexec bgw sysctl net.inet.ip.forwarding=1
+
+ # host b
+ vnet_mkjail b ${blan}a
+ jexec b ifconfig ${blan}a 4.0.0.44/24 up
+ jexec b route add default 4.0.0.1
+
+ # Office A VPN setup
+ echo '
+ spdadd 1.0.0.0/24 4.0.0.0/24 any -P out ipsec esp/tunnel/2.0.0.22-3.0.0.33/require;
+ spdadd 4.0.0.0/24 1.0.0.0/24 any -P in ipsec esp/tunnel/3.0.0.33-2.0.0.22/require;
+ add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456";
+ add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456";
+ ' | jexec agw setkey -c
+
+ # Office B VPN setup
+ echo '
+ spdadd 4.0.0.0/24 1.0.0.0/24 any -P out ipsec esp/tunnel/3.0.0.33-2.0.0.22/require;
+ spdadd 1.0.0.0/24 4.0.0.0/24 any -P in ipsec esp/tunnel/2.0.0.22-3.0.0.33/require;
+ add 2.0.0.22 3.0.0.33 esp 0x203 -E aes-gcm-16 "123456789012345678901234567890123456";
+ add 3.0.0.33 2.0.0.22 esp 0x302 -E aes-gcm-16 "123456789012345678901234567890123456";
+ ' | jexec bgw setkey -c
+}
+
+atf_test_case "ip4_pfil_in_after_stripping" "cleanup"
+ip4_pfil_in_after_stripping_head()
+{
+ atf_set descr 'Test that pf pulls up mbuf if m_len==0 after stripping the outer header'
+ atf_set require.user root
+ atf_set require.progs nc
+}
+ip4_pfil_in_after_stripping_body()
+{
+ pft_init
+ if_enc_init
+
+ build_test_network
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore jexec a ping -c3 4.0.0.44
+
+ # Configure port forwarding on host bgw
+ jexec bgw ifconfig enc0 up
+ jexec bgw sysctl net.inet.ipsec.filtertunnel=0
+ jexec bgw sysctl net.enc.in.ipsec_filter_mask=2 # after stripping
+ jexec bgw sysctl net.enc.out.ipsec_filter_mask=1 # before outer header
+ jexec bgw pfctl -e
+ pft_set_rules bgw \
+ "rdr on enc0 proto tcp to 4.0.0.1 port 666 -> 4.0.0.44" \
+ "pass"
+
+ # Prepare the catcher on host b
+ echo "unexpected" > ./receiver
+ jexec b nc -n4l -N 666 > ./receiver &
+ nc_pid=$!
+ sleep 1
+
+ # Poke it from host a to host bgw
+ spell="Ak Ohum Oktay Weez Barsoom."
+ echo $spell | jexec a nc -w3 4.0.0.1 666
+
+ # Expect it to hit host b instead
+ sleep 1 # let the catcher finish
+ jexec b kill -KILL $nc_pid # in a fail case the catcher may listen forever
+ atf_check_equal "$spell" "$(cat ./receiver)"
+}
+ip4_pfil_in_after_stripping_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "ip4_pfil_in_after_stripping"
+}
diff --git a/tests/sys/netpfil/pf/ioctl/Makefile b/tests/sys/netpfil/pf/ioctl/Makefile
index 5d4e9dbcff5f..6bcf48432d30 100644
--- a/tests/sys/netpfil/pf/ioctl/Makefile
+++ b/tests/sys/netpfil/pf/ioctl/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/netpfil/pf/ioctl
diff --git a/tests/sys/netpfil/pf/killstate.sh b/tests/sys/netpfil/pf/killstate.sh
index 046d640ed355..447a4e388f11 100644
--- a/tests/sys/netpfil/pf/killstate.sh
+++ b/tests/sys/netpfil/pf/killstate.sh
@@ -47,7 +47,7 @@ v4_head()
{
atf_set descr 'Test killing states by IPv4 address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v4_body()
@@ -110,7 +110,7 @@ v6_head()
{
atf_set descr 'Test killing states by IPv6 address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
@@ -177,7 +177,7 @@ label_head()
{
atf_set descr 'Test killing states by label'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
label_body()
@@ -241,7 +241,7 @@ multilabel_head()
{
atf_set descr 'Test killing states with multiple labels by label'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
multilabel_body()
@@ -321,7 +321,7 @@ gateway_head()
{
atf_set descr 'Test killing states by route-to/reply-to address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
gateway_body()
@@ -410,7 +410,7 @@ match_body()
vnet_mkjail singsing ${epair_two}b
jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up
jexec singsing route add default 198.51.100.1
- jexec singsing /usr/sbin/inetd -p inetd-echo.pid \
+ jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-echo.pid \
$(atf_get_srcdir)/echo_inetd.conf
route add 198.51.100.0/24 192.0.2.2
@@ -462,7 +462,7 @@ interface_head()
{
atf_set descr 'Test killing states based on interface'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
interface_body()
@@ -518,7 +518,7 @@ id_head()
{
atf_set descr 'Test killing states by id'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
id_body()
@@ -574,12 +574,68 @@ id_cleanup()
pft_cleanup
}
+atf_test_case "key" "cleanup"
+key_head()
+{
+ atf_set descr 'Test killing states by their key'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+key_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a 192.0.2.1/24 up
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz pfctl -e
+
+ pft_set_rules alcatraz \
+ "block all" \
+ "pass in proto tcp" \
+ "pass in proto icmp"
+
+ # Sanity check & establish state
+ atf_check -s exit:0 -o ignore ${common_dir}/pft_ping.py \
+ --sendif ${epair}a \
+ --to 192.0.2.2 \
+ --replyif ${epair}a
+
+ # Get the state key
+ key=$(jexec alcatraz pfctl -ss -vvv | awk '/icmp/ { print($2 " " $3 " " $4 " " $5); }')
+ bad_key=$(echo ${key} | sed 's/icmp/tcp/')
+
+ # Kill the wrong key
+ atf_check -s exit:0 -e "match:killed 0 states" \
+ jexec alcatraz pfctl -k key -k "${bad_key}"
+ if ! find_state;
+ then
+ atf_fail "Killing a different ID removed the state."
+ fi
+
+ # Kill the correct key
+ atf_check -s exit:0 -e "match:killed 1 states" \
+ jexec alcatraz pfctl -k key -k "${key}"
+ if find_state;
+ then
+ atf_fail "Killing the state did not remove it."
+ fi
+}
+
+key_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "nat" "cleanup"
nat_head()
{
atf_set descr 'Test killing states by their NAT-ed IP address'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
nat_body()
@@ -653,5 +709,6 @@ atf_init_test_cases()
atf_add_test_case "match"
atf_add_test_case "interface"
atf_add_test_case "id"
+ atf_add_test_case "key"
atf_add_test_case "nat"
}
diff --git a/tests/sys/netpfil/pf/limits.sh b/tests/sys/netpfil/pf/limits.sh
new file mode 100644
index 000000000000..69f0b6af2ccf
--- /dev/null
+++ b/tests/sys/netpfil/pf/limits.sh
@@ -0,0 +1,119 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Kristof Provost <kp@FreeBSD.org>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Test setting and retrieving limits'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ vnet_mkjail alcatraz
+
+ pft_set_rules alcatraz \
+ "set limit states 200" \
+ "set limit frags 100" \
+ "set limit src-nodes 50" \
+ "set limit table-entries 25"
+
+ atf_check -s exit:0 -o match:'states.*200' \
+ jexec alcatraz pfctl -sm
+ atf_check -s exit:0 -o match:'frags.*100' \
+ jexec alcatraz pfctl -sm
+ atf_check -s exit:0 -o match:'src-nodes.*50' \
+ jexec alcatraz pfctl -sm
+ atf_check -s exit:0 -o match:'table-entries.*25' \
+ jexec alcatraz pfctl -sm
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "zero" "cleanup"
+zero_head()
+{
+ atf_set descr 'Test changing a limit from zero on an in-use zone'
+ atf_set require.user root
+}
+
+zero_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 3 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ # Set no limit
+ pft_set_rules noflush alcatraz \
+ "set limit states 0" \
+ "pass"
+
+ # Check that we really report no limit
+ atf_check -s exit:0 -o 'match:states hard limit 0' \
+ jexec alcatraz pfctl -sa
+
+ # Create a state
+ atf_check -s exit:0 -o ignore \
+ ping -c 3 192.0.2.1
+
+ # Limit states
+ pft_set_rules noflush alcatraz \
+ "set limit states 1000" \
+ "pass"
+
+ # And create a new state
+ atf_check -s exit:0 -o ignore \
+ ping -c 3 192.0.2.1
+
+ atf_check -s exit:0 -o 'match:states hard limit 1000' \
+ jexec alcatraz pfctl -sa
+}
+
+zero_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+ atf_add_test_case "zero"
+}
diff --git a/tests/sys/netpfil/pf/map_e.sh b/tests/sys/netpfil/pf/map_e.sh
deleted file mode 100644
index 742264dcf547..000000000000
--- a/tests/sys/netpfil/pf/map_e.sh
+++ /dev/null
@@ -1,91 +0,0 @@
-#
-# SPDX-License-Identifier: BSD-2-Clause
-#
-# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com>
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions
-# are met:
-# 1. Redistributions of source code must retain the above copyright
-# notice, this list of conditions and the following disclaimer.
-# 2. Redistributions in binary form must reproduce the above copyright
-# notice, this list of conditions and the following disclaimer in the
-# documentation and/or other materials provided with the distribution.
-#
-# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
-# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
-# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
-# SUCH DAMAGE.
-
-. $(atf_get_srcdir)/utils.subr
-
-atf_test_case "map_e" "cleanup"
-map_e_head()
-{
- atf_set descr 'map-e-portset test'
- atf_set require.user root
-}
-
-map_e_body()
-{
- NC_TRY_COUNT=12
-
- pft_init
-
- epair_map_e=$(vnet_mkepair)
- epair_echo=$(vnet_mkepair)
-
- vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a
- vnet_mkjail echo ${epair_echo}b
-
- ifconfig ${epair_map_e}a 192.0.2.2/24 up
- route add -net 198.51.100.0/24 192.0.2.1
-
- jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up
- jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up
- jexec map_e sysctl net.inet.ip.forwarding=1
-
- jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up
- jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf
-
- # Enable pf!
- jexec map_e pfctl -e
- pft_set_rules map_e \
- "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342"
-
- # Only allow specified ports.
- jexec echo pfctl -e
- pft_set_rules echo "block return all" \
- "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \
- "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \
- "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \
- "set skip on lo"
-
- i=0
- while [ ${i} -lt ${NC_TRY_COUNT} ]
- do
- echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7
- if [ $? -ne 0 ]; then
- atf_fail "nc failed (${i})"
- fi
- i=$((${i}+1))
- done
-}
-
-map_e_cleanup()
-{
- rm -f inetd-echo.pid
- pft_cleanup
-}
-
-atf_init_test_cases()
-{
- atf_add_test_case "map_e"
-}
diff --git a/tests/sys/netpfil/pf/match.sh b/tests/sys/netpfil/pf/match.sh
index bb088c5bf47c..58c1e021310a 100644
--- a/tests/sys/netpfil/pf/match.sh
+++ b/tests/sys/netpfil/pf/match.sh
@@ -26,6 +26,8 @@
. $(atf_get_srcdir)/utils.subr
+common_dir=$(atf_get_srcdir)/../common
+
atf_test_case "dummynet" "cleanup"
dummynet_head()
{
@@ -67,7 +69,117 @@ dummynet_cleanup()
pft_cleanup
}
+atf_test_case "quick" "cleanup"
+quick_head()
+{
+ atf_set descr 'Test quick on match rules'
+ atf_set require.user root
+}
+
+quick_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}b
+
+ ifconfig ${epair}a 192.0.2.1/24 up
+ jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.2
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "pass" \
+ "match in quick proto icmp" \
+ "block"
+
+ # 'match quick' should retain the previous pass/block state
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.2
+
+ pft_set_rules alcatraz \
+ "block" \
+ "match in quick proto icmp" \
+ "pass"
+
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 192.0.2.2
+}
+
+quick_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "allow_opts" "cleanup"
+allow_opts_head()
+{
+ atf_set descr 'Test allowing IP options via match'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+allow_opts_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz pfctl -x loud
+ pft_set_rules alcatraz \
+ "match proto icmp allow-opts" \
+ "pass"
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ atf_check -s exit:0 -o ignore \
+ ${common_dir}/pft_ping.py \
+ --sendif ${epair}b \
+ --to 192.0.2.1 \
+ --send-nop \
+ --replyif ${epair}b
+
+ # This doesn't work without 'allow-opts'
+ pft_set_rules alcatraz \
+ "match proto icmp" \
+ "pass"
+ atf_check -s exit:1 -o ignore \
+ ${common_dir}/pft_ping.py \
+ --sendif ${epair}b \
+ --to 192.0.2.1 \
+ --send-nop \
+ --replyif ${epair}b
+
+ # Setting it on a pass rule still works.
+ pft_set_rules alcatraz \
+ "pass allow-opts"
+ atf_check -s exit:0 -o ignore \
+ ${common_dir}/pft_ping.py \
+ --sendif ${epair}b \
+ --to 192.0.2.1 \
+ --send-nop \
+ --replyif ${epair}b
+}
+
+allow_opts_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "dummynet"
+ atf_add_test_case "quick"
+ atf_add_test_case "allow_opts"
}
diff --git a/tests/sys/netpfil/pf/max_pkt_rate.sh b/tests/sys/netpfil/pf/max_pkt_rate.sh
new file mode 100644
index 000000000000..bdd140eb60dd
--- /dev/null
+++ b/tests/sys/netpfil/pf/max_pkt_rate.sh
@@ -0,0 +1,121 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+common_setup()
+{
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b inet 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+}
+
+common_test()
+{
+ # One ping will pass
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # As will a second
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # But the third should fail
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # But three seconds later we can ping again
+ sleep 3
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+}
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Basic maximum packet rate test'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ common_setup
+
+ pft_set_rules alcatraz \
+ "block" \
+ "pass in proto icmp max-pkt-rate 2/2"
+
+ common_test
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "anchor" "cleanup"
+anchor_head()
+{
+ atf_set descr 'maximum packet rate on anchor'
+ atf_set require.user root
+}
+
+anchor_body()
+{
+ pft_init
+
+ common_setup
+
+ pft_set_rules alcatraz \
+ "block" \
+ "anchor \"foo\" proto icmp max-pkt-rate 2/2 {\n \
+ pass \n \
+ }"
+
+ common_test
+}
+
+anchor_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+ atf_add_test_case "anchor"
+}
diff --git a/tests/sys/netpfil/pf/max_pkt_size.sh b/tests/sys/netpfil/pf/max_pkt_size.sh
new file mode 100644
index 000000000000..030d642303fc
--- /dev/null
+++ b/tests/sys/netpfil/pf/max_pkt_size.sh
@@ -0,0 +1,122 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+common_setup()
+{
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ jexec alcatraz pfctl -e
+}
+
+common_test()
+{
+ # Small packets pass
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 -s 100 192.0.2.1
+
+ # Larger packets do not
+ atf_check -s exit:2 -o ignore \
+ ping -c 3 -s 101 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 3 -s 128 192.0.2.1
+}
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Basic max-pkt-size test'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ common_setup
+
+ pft_set_rules alcatraz \
+ "pass max-pkt-size 128"
+
+ common_test
+
+ # We can enforce this on fragmented packets too
+ pft_set_rules alcatraz \
+ "pass max-pkt-size 2000"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 -s 1400 192.0.2.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 -s 1972 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 -s 1973 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 3 -s 3000 192.0.2.1
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "match" "cleanup"
+match_head()
+{
+ atf_set descr 'max-pkt-size on match rules'
+ atf_set require.user root
+}
+
+match_body()
+{
+ pft_init
+
+ common_setup
+
+ pft_set_rules alcatraz \
+ "match in max-pkt-size 128" \
+ "pass"
+
+ common_test
+}
+
+match_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+ atf_add_test_case "match"
+}
diff --git a/tests/sys/netpfil/pf/max_states.sh b/tests/sys/netpfil/pf/max_states.sh
new file mode 100755
index 000000000000..1bf6814f9283
--- /dev/null
+++ b/tests/sys/netpfil/pf/max_states.sh
@@ -0,0 +1,62 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+
+max_states_head()
+{
+ atf_set descr 'Max states per rule'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+max_states_body()
+{
+ setup_router_dummy_ipv6
+
+ pft_set_rules router \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in on ${epair_tester}b inet6 proto tcp keep state (max 3)" \
+ "pass out on ${epair_server}a inet6 proto tcp keep state"
+
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4201
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4202
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4203
+ ping_dummy_check_request exit:1 --ping-type=tcpsyn --send-sport=4204
+}
+
+max_states_cleanup()
+{
+ pft_cleanup
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case "max_states"
+}
diff --git a/tests/sys/netpfil/pf/mbuf.sh b/tests/sys/netpfil/pf/mbuf.sh
new file mode 100644
index 000000000000..d845f793a969
--- /dev/null
+++ b/tests/sys/netpfil/pf/mbuf.sh
@@ -0,0 +1,236 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Igor Ostapenko <pm@igoro.pro>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+dummymbuf_init()
+{
+ if ! kldstat -q -m dummymbuf; then
+ atf_skip "This test requires dummymbuf"
+ fi
+}
+
+atf_test_case "inet_in_mbuf_len" "cleanup"
+inet_in_mbuf_len_head()
+{
+ atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip)'
+ atf_set require.user root
+}
+inet_in_mbuf_len_body()
+{
+ pft_init
+ dummymbuf_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Set up a simple jail with one interface
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ # Should be denied
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block"
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
+
+ # Should be allowed by from/to addresses
+ pft_set_rules alcatraz \
+ "block" \
+ "pass in from 192.0.2.1 to 192.0.2.2"
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ # Should still work for m_len=0
+ jexec alcatraz pfilctl link -i dummymbuf:inet inet
+ jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 0;"
+ atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=1
+ jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 1;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=19
+ # provided IPv4 basic header is 20 bytes long, it should impact the dst addr
+ jexec alcatraz sysctl net.dummymbuf.rules="inet in ${epair}b pull-head 19;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+}
+inet_in_mbuf_len_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "inet6_in_mbuf_len" "cleanup"
+inet6_in_mbuf_len_head()
+{
+ atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ip6_hdr)'
+ atf_set require.user root
+}
+inet6_in_mbuf_len_body()
+{
+ pft_init
+ dummymbuf_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a inet6 2001:db8::1/64 up no_dad
+
+ # Set up a simple jail with one interface
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b inet6 2001:db8::2/64 up no_dad
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c1 2001:db8::2
+
+ # Should be denied
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block" \
+ "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }"
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 2001:db8::2
+
+ # Avoid redundant ICMPv6 packets to avoid false positives during
+ # counting of net.dummymbuf.hits.
+ ndp -i ${epair}a -- -nud
+ jexec alcatraz ndp -i ${epair}b -- -nud
+
+ # Should be allowed by from/to addresses
+ pft_set_rules alcatraz \
+ "block" \
+ "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in inet6 from 2001:db8::1 to 2001:db8::2"
+ atf_check -s exit:0 -o ignore ping -c1 2001:db8::2
+
+ # Should still work for m_len=0
+ jexec alcatraz pfilctl link -i dummymbuf:inet6 inet6
+ jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 0;"
+ atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+ atf_check -s exit:0 -o ignore ping -c1 2001:db8::2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=1
+ jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 1;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 2001:db8::2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=39
+ # provided IPv6 basic header is 40 bytes long, it should impact the dst addr
+ jexec alcatraz sysctl net.dummymbuf.rules="inet6 in ${epair}b pull-head 39;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 2001:db8::2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+}
+inet6_in_mbuf_len_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "ethernet_in_mbuf_len" "cleanup"
+ethernet_in_mbuf_len_head()
+{
+ atf_set descr 'Test that pf can handle inbound with the first mbuf with m_len < sizeof(struct ether_header)'
+ atf_set require.user root
+}
+ethernet_in_mbuf_len_body()
+{
+ pft_init
+ dummymbuf_init
+
+ epair=$(vnet_mkepair)
+ epair_a_mac=$(ifconfig ${epair}a ether | awk '/ether/ { print $2; }')
+ ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Set up a simple jail with one interface
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+ epair_b_mac=$(jexec alcatraz ifconfig ${epair}b ether | awk '/ether/ { print $2; }')
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ # Should be denied
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "ether block" \
+ "pass"
+ atf_check -s not-exit:0 -o ignore ping -c1 -t1 192.0.2.2
+
+ # Should be allowed by from/to addresses
+ echo $epair_a_mac
+ echo $epair_b_mac
+ pft_set_rules alcatraz \
+ "ether block" \
+ "ether pass in from ${epair_a_mac} to ${epair_b_mac}" \
+ "ether pass out from ${epair_b_mac} to ${epair_a_mac}" \
+ "pass"
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+ # Should still work for m_len=0
+ jexec alcatraz pfilctl link -i dummymbuf:ethernet ethernet
+ jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 0;"
+ atf_check_equal "0" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=1
+ jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 1;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=11
+ # for the simplest L2 Ethernet frame it should impact src field
+ jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 11;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+
+ # m_len=13
+ # provided L2 Ethernet simplest header is 14 bytes long, it should impact ethertype field
+ jexec alcatraz sysctl net.dummymbuf.rules="ethernet in ${epair}b pull-head 13;"
+ jexec alcatraz sysctl net.dummymbuf.hits=0
+ atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+ atf_check_equal "1" "$(jexec alcatraz sysctl -n net.dummymbuf.hits)"
+}
+ethernet_in_mbuf_len_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "inet_in_mbuf_len"
+ atf_add_test_case "inet6_in_mbuf_len"
+ atf_add_test_case "ethernet_in_mbuf_len"
+}
diff --git a/tests/sys/netpfil/pf/modulate.sh b/tests/sys/netpfil/pf/modulate.sh
index 98d5df14a44d..1abe22cff391 100644
--- a/tests/sys/netpfil/pf/modulate.sh
+++ b/tests/sys/netpfil/pf/modulate.sh
@@ -31,7 +31,7 @@ modulate_v4_head()
{
atf_set descr 'IPv4 TCP sequence number modulation'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
modulate_v4_body()
@@ -54,7 +54,7 @@ modulate_v6_head()
{
atf_set descr 'IPv6 TCP sequence number modulation'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
modulate_v6_body()
diff --git a/tests/sys/netpfil/pf/nat.sh b/tests/sys/netpfil/pf/nat.sh
index 7cc0d8f35c96..f1fdf6405d97 100644
--- a/tests/sys/netpfil/pf/nat.sh
+++ b/tests/sys/netpfil/pf/nat.sh
@@ -2,6 +2,8 @@
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2018 Kristof Provost <kp@FreeBSD.org>
+# Copyright (c) 2025 Kajetan Staszkiewicz <ks@FreeBSD.org>
+# Copyright (c) 2021 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
@@ -51,7 +53,7 @@ exhaust_body()
jexec nat sysctl net.inet.ip.forwarding=1
jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up
- jexec echo /usr/sbin/inetd -p inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf
+ jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf
# Enable pf!
jexec nat pfctl -e
@@ -79,7 +81,6 @@ exhaust_body()
exhaust_cleanup()
{
- rm -f inetd-echo.pid
pft_cleanup
}
@@ -113,13 +114,684 @@ nested_anchor_body()
}
+endpoint_independent_setup()
+{
+ pft_init
+ filter="udp and dst port 1234" # only capture udp pings
+
+ epair_client=$(vnet_mkepair)
+ epair_nat=$(vnet_mkepair)
+ epair_server1=$(vnet_mkepair)
+ epair_server2=$(vnet_mkepair)
+ bridge=$(vnet_mkbridge)
+
+ vnet_mkjail nat ${epair_client}b ${epair_nat}a
+ vnet_mkjail client ${epair_client}a
+ vnet_mkjail server1 ${epair_server1}a
+ vnet_mkjail server2 ${epair_server2}a
+
+ ifconfig ${epair_server1}b up
+ ifconfig ${epair_server2}b up
+ ifconfig ${epair_nat}b up
+ ifconfig ${bridge} \
+ addm ${epair_server1}b \
+ addm ${epair_server2}b \
+ addm ${epair_nat}b \
+ up
+
+ jexec nat ifconfig ${epair_client}b 192.0.2.1/24 up
+ jexec nat ifconfig ${epair_nat}a 198.51.100.42/24 up
+ jexec nat sysctl net.inet.ip.forwarding=1
+
+ jexec client ifconfig ${epair_client}a 192.0.2.2/24 up
+ jexec client route add default 192.0.2.1
+
+ jexec server1 ifconfig ${epair_server1}a 198.51.100.32/24 up
+ jexec server2 ifconfig ${epair_server2}a 198.51.100.22/24 up
+}
+
+endpoint_independent_common()
+{
+ # Enable pf!
+ jexec nat pfctl -e
+
+ # validate non-endpoint independent nat rule behaviour
+ pft_set_rules nat "${1}"
+
+ jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \
+ --immediate-mode $filter &
+ server1tcppid="$!"
+ jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \
+ --immediate-mode $filter &
+ server2tcppid="$!"
+
+ # send out multiple packets
+ for i in $(seq 1 10); do
+ echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0
+ echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0
+ done
+
+ kill $server1tcppid
+ kill $server2tcppid
+
+ tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}')
+ tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}')
+
+ if [ -z $tuple_server1 ]
+ then
+ atf_fail "server1 did not receive connection from client (default)"
+ fi
+
+ if [ -z $tuple_server2 ]
+ then
+ atf_fail "server2 did not receive connection from client (default)"
+ fi
+
+ if [ "$tuple_server1" = "$tuple_server2" ]
+ then
+ echo "server1 tcpdump: $tuple_server1"
+ echo "server2 tcpdump: $tuple_server2"
+ atf_fail "Received same IP:port on server1 and server2 (default)"
+ fi
+
+ # validate endpoint independent nat rule behaviour
+ pft_set_rules nat "${2}"
+
+ jexec server1 tcpdump -i ${epair_server1}a -w ${PWD}/server1.pcap \
+ --immediate-mode $filter &
+ server1tcppid="$!"
+ jexec server2 tcpdump -i ${epair_server2}a -w ${PWD}/server2.pcap \
+ --immediate-mode $filter &
+ server2tcppid="$!"
+
+ # send out multiple packets, sometimes one fails to go through
+ for i in $(seq 1 10); do
+ echo "ping" | jexec client nc -u 198.51.100.32 1234 -p 4242 -w 0
+ echo "ping" | jexec client nc -u 198.51.100.22 1234 -p 4242 -w 0
+ done
+
+ kill $server1tcppid
+ kill $server2tcppid
+
+ tuple_server1=$(tcpdump -r ${PWD}/server1.pcap | awk '{addr=$3} END {print addr}')
+ tuple_server2=$(tcpdump -r ${PWD}/server2.pcap | awk '{addr=$3} END {print addr}')
+
+ if [ -z $tuple_server1 ]
+ then
+ atf_fail "server1 did not receive connection from client (endpoint-independent)"
+ fi
+
+ if [ -z $tuple_server2 ]
+ then
+ atf_fail "server2 did not receive connection from client (endpoint-independent)"
+ fi
+
+ if [ ! "$tuple_server1" = "$tuple_server2" ]
+ then
+ echo "server1 tcpdump: $tuple_server1"
+ echo "server2 tcpdump: $tuple_server2"
+ atf_fail "Received different IP:port on server1 than server2 (endpoint-independent)"
+ fi
+}
+
+atf_test_case "endpoint_independent_compat" "cleanup"
+endpoint_independent_compat_head()
+{
+ atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers'
+ atf_set require.user root
+}
+
+endpoint_independent_compat_body()
+{
+ endpoint_independent_setup # Sets ${epair_…} variables
+
+ endpoint_independent_common \
+ "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a)" \
+ "nat on ${epair_nat}a inet from ! (${epair_nat}a) to any -> (${epair_nat}a) endpoint-independent"
+}
+
+endpoint_independent_compat_cleanup()
+{
+ pft_cleanup
+ rm -f server1.out
+ rm -f server2.out
+}
+
+atf_test_case "endpoint_independent_pass" "cleanup"
+endpoint_independent_pass_head()
+{
+ atf_set descr 'Test that a client behind NAT gets the same external IP:port for different servers'
+ atf_set require.user root
+}
+
+endpoint_independent_pass_body()
+{
+ endpoint_independent_setup # Sets ${epair_…} variables
+
+ endpoint_independent_common \
+ "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) keep state" \
+ "pass out on ${epair_nat}a inet from ! (${epair_nat}a) to any nat-to (${epair_nat}a) endpoint-independent keep state"
+
+}
+
+endpoint_independent_pass_cleanup()
+{
+ pft_cleanup
+ rm -f server1.out
+ rm -f server2.out
+}
+
nested_anchor_cleanup()
{
pft_cleanup
}
+atf_test_case "nat6_nolinklocal" "cleanup"
+nat6_nolinklocal_head()
+{
+ atf_set descr 'Ensure we do not use link-local addresses'
+ atf_set require.user root
+}
+
+nat6_nolinklocal_body()
+{
+ pft_init
+
+ epair_nat=$(vnet_mkepair)
+ epair_echo=$(vnet_mkepair)
+
+ vnet_mkjail nat ${epair_nat}b ${epair_echo}a
+ vnet_mkjail echo ${epair_echo}b
+
+ ifconfig ${epair_nat}a inet6 2001:db8::2/64 no_dad up
+ route add -6 -net 2001:db8:1::/64 2001:db8::1
+
+ jexec nat ifconfig ${epair_nat}b inet6 2001:db8::1/64 no_dad up
+ jexec nat ifconfig ${epair_echo}a inet6 2001:db8:1::1/64 no_dad up
+ jexec nat sysctl net.inet6.ip6.forwarding=1
+
+ jexec echo ifconfig ${epair_echo}b inet6 2001:db8:1::2/64 no_dad up
+ # Ensure we can't reply to link-local pings
+ jexec echo pfctl -e
+ pft_set_rules echo \
+ "pass" \
+ "block in inet6 proto icmp6 from fe80::/10 to any icmp6-type echoreq"
+
+ jexec nat pfctl -e
+ pft_set_rules nat \
+ "nat pass on ${epair_echo}a inet6 from 2001:db8::/64 to any -> (${epair_echo}a)" \
+ "pass"
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 2001:db8::1
+ for i in `seq 0 10`
+ do
+ atf_check -s exit:0 -o ignore \
+ ping -6 -c 1 2001:db8:1::2
+ done
+}
+
+nat6_nolinklocal_cleanup()
+{
+ pft_cleanup
+}
+
+empty_table_common()
+{
+ option=$1
+
+ pft_init
+
+ epair_wan=$(vnet_mkepair)
+ epair_lan=$(vnet_mkepair)
+
+ vnet_mkjail srv ${epair_wan}a
+ jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up
+
+ vnet_mkjail rtr ${epair_wan}b ${epair_lan}a
+ jexec rtr ifconfig ${epair_wan}b 192.0.2.1/24 up
+ jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up
+ jexec rtr sysctl net.inet.ip.forwarding=1
+
+ ifconfig ${epair_lan}b 198.51.100.2/24 up
+ route add default 198.51.100.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "table <empty>" \
+ "nat on ${epair_wan}b inet from 198.51.100.0/24 -> <empty> ${option}" \
+ "pass"
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ jexec rtr ping -c 1 192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 198.51.100.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # Provoke divide by zero
+ ping -c 1 192.0.2.2
+ true
+}
+
+atf_test_case "empty_table_source_hash" "cleanup"
+empty_table_source_hash_head()
+{
+ atf_set descr 'Test source-hash on an emtpy table'
+ atf_set require.user root
+}
+
+empty_table_source_hash_body()
+{
+ empty_table_common "source-hash"
+}
+
+empty_table_source_hash_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "empty_table_random" "cleanup"
+empty_table_random_head()
+{
+ atf_set descr 'Test random on an emtpy table'
+ atf_set require.user root
+}
+
+empty_table_random_body()
+{
+ empty_table_common "random"
+}
+
+empty_table_random_cleanup()
+{
+ pft_cleanup
+}
+
+no_addrs_common()
+{
+ option=$1
+
+ pft_init
+
+ epair_wan=$(vnet_mkepair)
+ epair_lan=$(vnet_mkepair)
+
+ vnet_mkjail srv ${epair_wan}a
+ jexec srv ifconfig ${epair_wan}a 192.0.2.2/24 up
+
+ vnet_mkjail rtr ${epair_wan}b ${epair_lan}a
+ jexec rtr route add -net 192.0.2.0/24 -iface ${epair_wan}b
+ jexec rtr ifconfig ${epair_lan}a 198.51.100.1/24 up
+ jexec rtr sysctl net.inet.ip.forwarding=1
+
+ ifconfig ${epair_lan}b 198.51.100.2/24 up
+ route add default 198.51.100.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "nat on ${epair_wan}b inet from 198.51.100.0/24 -> (${epair_wan}b) ${option}" \
+ "pass"
+
+ # Provoke divide by zero
+ ping -c 1 192.0.2.2
+ true
+}
+
+atf_test_case "no_addrs_source_hash" "cleanup"
+no_addrs_source_hash_head()
+{
+ atf_set descr 'Test source-hash on an interface with no addresses'
+ atf_set require.user root
+}
+
+no_addrs_source_hash_body()
+{
+ no_addrs_common "source-hash"
+}
+
+no_addrs_source_hash_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "no_addrs_random" "cleanup"
+no_addrs_random_head()
+{
+ atf_set descr 'Test random on an interface with no addresses'
+ atf_set require.user root
+}
+
+no_addrs_random_body()
+{
+ no_addrs_common "random"
+}
+
+no_addrs_random_cleanup()
+{
+ pft_cleanup
+}
+
+nat_pass_head()
+{
+ atf_set descr 'IPv4 NAT on pass rule'
+ atf_set require.user root
+ atf_set require.progs scapy
+}
+
+nat_pass_body()
+{
+ setup_router_server_ipv4
+ # Delete the route back to make sure that the traffic has been NAT-ed
+ jexec server route del -net ${net_tester} ${net_server_host_router}
+
+ pft_set_rules router \
+ "block" \
+ "pass in on ${epair_tester}b inet proto tcp keep state" \
+ "pass out on ${epair_server}a inet proto tcp nat-to ${epair_server}a keep state"
+
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201
+
+ jexec router pfctl -qvvsr
+ jexec router pfctl -qvvss
+ jexec router ifconfig
+ jexec router netstat -rn
+}
+
+nat_pass_cleanup()
+{
+ pft_cleanup
+}
+
+nat_match_head()
+{
+ atf_set descr 'IPv4 NAT on match rule'
+ atf_set require.user root
+ atf_set require.progs scapy
+}
+
+nat_match_body()
+{
+ setup_router_server_ipv4
+ # Delete the route back to make sure that the traffic has been NAT-ed
+ jexec server route del -net ${net_tester} ${net_server_host_router}
+
+ # NAT is applied during ruleset evaluation:
+ # rules after "match" match on NAT-ed address
+ pft_set_rules router \
+ "block" \
+ "pass in on ${epair_tester}b inet proto tcp keep state" \
+ "match out on ${epair_server}a inet proto tcp nat-to ${epair_server}a" \
+ "pass out on ${epair_server}a inet proto tcp from ${epair_server}a keep state"
+
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201
+
+ jexec router pfctl -qvvsr
+ jexec router pfctl -qvvss
+ jexec router ifconfig
+ jexec router netstat -rn
+}
+
+nat_match_cleanup()
+{
+ pft_cleanup
+}
+
+map_e_common()
+{
+ NC_TRY_COUNT=12
+
+ pft_init
+
+ epair_map_e=$(vnet_mkepair)
+ epair_echo=$(vnet_mkepair)
+
+ vnet_mkjail map_e ${epair_map_e}b ${epair_echo}a
+ vnet_mkjail echo ${epair_echo}b
+
+ ifconfig ${epair_map_e}a 192.0.2.2/24 up
+ route add -net 198.51.100.0/24 192.0.2.1
+
+ jexec map_e ifconfig ${epair_map_e}b 192.0.2.1/24 up
+ jexec map_e ifconfig ${epair_echo}a 198.51.100.1/24 up
+ jexec map_e sysctl net.inet.ip.forwarding=1
+
+ jexec echo ifconfig ${epair_echo}b 198.51.100.2/24 up
+ jexec echo /usr/sbin/inetd -p ${PWD}/inetd-echo.pid $(atf_get_srcdir)/echo_inetd.conf
+
+ # Enable pf!
+ jexec map_e pfctl -e
+}
+
+atf_test_case "map_e_compat" "cleanup"
+map_e_compat_head()
+{
+ atf_set descr 'map-e-portset test'
+ atf_set require.user root
+}
+
+map_e_compat_body()
+{
+ map_e_common
+
+ pft_set_rules map_e \
+ "nat pass on ${epair_echo}a inet from 192.0.2.0/24 to any -> (${epair_echo}a) map-e-portset 2/12/0x342"
+
+ # Only allow specified ports.
+ jexec echo pfctl -e
+ pft_set_rules echo "block return all" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \
+ "set skip on lo"
+
+ i=0
+ while [ ${i} -lt ${NC_TRY_COUNT} ]
+ do
+ echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7
+ if [ $? -ne 0 ]; then
+ atf_fail "nc failed (${i})"
+ fi
+ i=$((${i}+1))
+ done
+}
+
+map_e_compat_cleanup()
+{
+ pft_cleanup
+}
+
+
+atf_test_case "map_e_pass" "cleanup"
+map_e_pass_head()
+{
+ atf_set descr 'map-e-portset test'
+ atf_set require.user root
+}
+
+map_e_pass_body()
+{
+ map_e_common
+
+ pft_set_rules map_e \
+ "pass out on ${epair_echo}a inet from 192.0.2.0/24 to any nat-to (${epair_echo}a) map-e-portset 2/12/0x342 keep state"
+
+ jexec map_e pfctl -qvvsr
+
+ # Only allow specified ports.
+ jexec echo pfctl -e
+ pft_set_rules echo "block return all" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 19720:19723 to (${epair_echo}b) port 7" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 36104:36107 to (${epair_echo}b) port 7" \
+ "pass in on ${epair_echo}b inet proto tcp from 198.51.100.1 port 52488:52491 to (${epair_echo}b) port 7" \
+ "set skip on lo"
+
+ i=0
+ while [ ${i} -lt ${NC_TRY_COUNT} ]
+ do
+ echo "foo ${i}" | timeout 2 nc -N 198.51.100.2 7
+ if [ $? -ne 0 ]; then
+ atf_fail "nc failed (${i})"
+ fi
+ i=$((${i}+1))
+ done
+}
+
+map_e_pass_cleanup()
+{
+ pft_cleanup
+}
+
+binat_compat_head()
+{
+ atf_set descr 'IPv4 BINAT with nat ruleset'
+ atf_set require.user root
+ atf_set require.progs scapy
+}
+
+binat_compat_body()
+{
+ setup_router_server_ipv4
+ # Delete the route back to make sure that the traffic has been NAT-ed
+ jexec server route del -net ${net_tester} ${net_server_host_router}
+
+ pft_set_rules router \
+ "set state-policy if-bound" \
+ "set ruleset-optimization none" \
+ "binat on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag -> ${epair_server}a" \
+ "block" \
+ "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \
+ "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \
+ "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \
+ "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state"
+
+ # Test the outbound NAT part of BINAT.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+
+ for state_regexp in \
+ "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 1" \
+ "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 2" \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+
+ # Test the inbound RDR part of BINAT.
+ # The "tester" becomes "server" and vice versa.
+ inetd_conf=$(mktemp)
+ echo "discard stream tcp nowait root internal" > $inetd_conf
+ inetd -p ${PWD}/inetd_tester.pid $inetd_conf
+
+ atf_check -s exit:0 \
+ jexec server ${common_dir}/pft_ping.py \
+ --ping-type=tcp3way --send-sport=4202 \
+ --sendif ${epair_server}b \
+ --to ${net_server_host_router} \
+ --replyif ${epair_server}b
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+
+ for state_regexp in \
+ "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 3" \
+ "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 4" \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+}
+
+binat_compat_cleanup()
+{
+ pft_cleanup
+ kill $(cat ${PWD}/inetd_tester.pid)
+}
+
+binat_match_head()
+{
+ atf_set descr 'IPv4 BINAT with nat ruleset'
+ atf_set require.user root
+ atf_set require.progs scapy
+}
+
+binat_match_body()
+{
+ setup_router_server_ipv4
+ # Delete the route back to make sure that the traffic has been NAT-ed
+ jexec server route del -net ${net_tester} ${net_server_host_router}
+
+ # The "binat-to" rule expands to 2 rules so the ""pass" rules start at 3!
+ pft_set_rules router \
+ "set state-policy if-bound" \
+ "set ruleset-optimization none" \
+ "block" \
+ "match on ${epair_server}a inet proto tcp from ${net_tester_host_tester} to any tag sometag binat-to ${epair_server}a" \
+ "pass in on ${epair_tester}b inet proto tcp !tagged sometag keep state" \
+ "pass out on ${epair_server}a inet proto tcp tagged sometag keep state" \
+ "pass in on ${epair_server}a inet proto tcp tagged sometag keep state" \
+ "pass out on ${epair_tester}b inet proto tcp tagged sometag keep state"
+
+ # Test the outbound NAT part of BINAT.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+
+ for state_regexp in \
+ "${epair_tester}b tcp ${net_server_host_server}:9 <- ${net_tester_host_tester}:4201 .* 3:2 pkts,.* rule 3" \
+ "${epair_server}a tcp ${net_server_host_router}:4201 \(${net_tester_host_tester}:4201\) -> ${net_server_host_server}:9 .* 3:2 pkts,.* rule 4" \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+
+ # Test the inbound RDR part of BINAT.
+ # The "tester" becomes "server" and vice versa.
+ inetd_conf=$(mktemp)
+ echo "discard stream tcp nowait root internal" > $inetd_conf
+ inetd -p ${PWD}/inetd_tester.pid $inetd_conf
+
+ atf_check -s exit:0 \
+ jexec server ${common_dir}/pft_ping.py \
+ --ping-type=tcp3way --send-sport=4202 \
+ --sendif ${epair_server}b \
+ --to ${net_server_host_router} \
+ --replyif ${epair_server}b
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+
+ for state_regexp in \
+ "${epair_server}a tcp ${net_tester_host_tester}:9 \(${net_server_host_router}:9\) <- ${net_server_host_server}:4202 .* 3:2 pkts,.* rule 5" \
+ "${epair_tester}b tcp ${net_server_host_server}:4202 -> ${net_tester_host_tester}:9 .* 3:2 pkts,.* rule 6" \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+}
+
+binat_match_cleanup()
+{
+ pft_cleanup
+ kill $(cat ${PWD}/inetd_tester.pid)
+}
+
atf_init_test_cases()
{
atf_add_test_case "exhaust"
atf_add_test_case "nested_anchor"
+ atf_add_test_case "endpoint_independent_compat"
+ atf_add_test_case "endpoint_independent_pass"
+ atf_add_test_case "nat6_nolinklocal"
+ atf_add_test_case "empty_table_source_hash"
+ atf_add_test_case "no_addrs_source_hash"
+ atf_add_test_case "empty_table_random"
+ atf_add_test_case "no_addrs_random"
+ atf_add_test_case "map_e_compat"
+ atf_add_test_case "map_e_pass"
+ atf_add_test_case "nat_pass"
+ atf_add_test_case "nat_match"
+ atf_add_test_case "binat_compat"
+ atf_add_test_case "binat_match"
}
diff --git a/tests/sys/netpfil/pf/nat64.py b/tests/sys/netpfil/pf/nat64.py
new file mode 100644
index 000000000000..adae2489ce5e
--- /dev/null
+++ b/tests/sys/netpfil/pf/nat64.py
@@ -0,0 +1,274 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import pytest
+import selectors
+import socket
+import sys
+from utils import DelayedSend
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+class TestNAT64(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "vnet3": {"ifaces": ["if2", "if3"]},
+ "vnet4": {"ifaces": ["if3"]},
+ "if1": {"prefixes6": [("2001:db8::2/64", "2001:db8::1/64")]},
+ "if2": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]},
+ "if3": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]}
+ }
+
+ def vnet4_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/route add default 198.51.100.1")
+
+ def vnet3_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.ttl=62")
+ ToolsHelper.print_output("/sbin/sysctl net.inet.udp.checksum=0")
+
+ sel = selectors.DefaultSelector()
+ t = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ t.bind(("0.0.0.0", 1234))
+ t.setblocking(False)
+ t.listen()
+ sel.register(t, selectors.EVENT_READ, data=None)
+
+ u = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+ u.bind(("0.0.0.0", 4444))
+ u.setblocking(False)
+ sel.register(u, selectors.EVENT_READ, data="UDP")
+
+ while True:
+ events = sel.select(timeout=20)
+ for key, mask in events:
+ sock = key.fileobj
+ if key.data is None:
+ conn, addr = sock.accept()
+ print(f"Accepted connection from {addr}")
+ data = types.SimpleNamespace(addr=addr, inb=b"", outb=b"")
+ events = selectors.EVENT_READ | selectors.EVENT_WRITE
+ sel.register(conn, events, data=data)
+ elif key.data == "UDP":
+ recv_data, addr = sock.recvfrom(1024)
+ print(f"Received UDP {recv_data} from {addr}")
+ sock.sendto(b"foo", addr)
+ else:
+ if mask & selectors.EVENT_READ:
+ recv_data = sock.recv(1024)
+ print(f"Received TCP {recv_data}")
+ sock.send(b"foo")
+ else:
+ print("Unknown event?")
+ t.close()
+ u.close()
+ return
+
+ def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
+
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.2")
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "pass inet6 proto icmp6",
+ "pass in on %s inet6 af-to inet from 192.0.2.1" % ifname])
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_tcp_rst(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+
+ import scapy.all as sp
+
+ # Send a SYN
+ packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \
+ / sp.TCP(dport=1222, flags="S")
+
+ # Get a reply
+ reply = sp.sr1(packet)
+
+ # We expect to get a RST here.
+ tcp = reply.getlayer(sp.TCP)
+ assert tcp
+ assert "R" in tcp.flags
+
+ # Now try to SYN to an open port
+ packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \
+ / sp.TCP(dport=1234, flags="S")
+ reply = sp.sr1(packet)
+
+ tcp = reply.getlayer(sp.TCP)
+ assert tcp
+
+ # We don't get RST
+ assert "R" not in tcp.flags
+
+ # We do get SYN|ACK
+ assert "S" in tcp.flags
+ assert "A" in tcp.flags
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_udp_port_closed(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \
+ / sp.UDP(dport=1222) / sp.Raw("bar")
+ reply = sp.sr1(packet, timeout=3)
+ print(reply.show())
+
+ # We expect an ICMPv6 error, not a UDP reply
+ assert not reply.getlayer(sp.UDP)
+ icmp = reply.getlayer(sp.ICMPv6DestUnreach)
+ assert icmp
+ assert icmp.type == 1
+ assert icmp.code == 4
+ udp = reply.getlayer(sp.UDPerror)
+ assert udp
+ assert udp.dport == 1222
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_address_unreachable(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::203.0.113.2") \
+ / sp.UDP(dport=1222) / sp.Raw("bar")
+ reply = sp.sr1(packet, timeout=3)
+ print(reply.show())
+
+ # We expect an ICMPv6 error, not a UDP reply
+ assert not reply.getlayer(sp.UDP)
+ icmp = reply.getlayer(sp.ICMPv6DestUnreach)
+ assert icmp
+ assert icmp.type == 1
+ assert icmp.code == 0
+ udp = reply.getlayer(sp.UDPerror)
+ assert udp
+ assert udp.dport == 1222
+
+ # Check the hop limit
+ ip6 = reply.getlayer(sp.IPv6)
+ assert ip6.hlim == 61
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_udp_checksum(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+
+ import scapy.all as sp
+
+ # Send an outbound UDP packet to establish state
+ packet = sp.IPv6(dst="64:ff9b::192.0.2.2") \
+ / sp.UDP(sport=3333, dport=4444) / sp.Raw("foo")
+
+ # Get a reply
+ # We'll send the reply without UDP checksum on the IPv4 side
+ # but that's not valid for IPv6, so expect pf to update the checksum.
+ reply = sp.sr1(packet, timeout=5)
+
+ udp = reply.getlayer(sp.UDP)
+ assert udp
+ assert udp.chksum != 0
+
+ def common_test_source_addr(self, packet):
+ vnet = self.vnet_map["vnet1"]
+ sendif = vnet.iface_alias_map["if1"].name
+
+ import scapy.all as sp
+
+ print("Outbound:\n")
+ packet.show()
+
+ s = DelayedSend(packet)
+
+ # We expect an ICMPv6 error here, where we'll verify the source address of
+ # the outer packet
+ packets = sp.sniff(iface=sendif, timeout=5)
+
+ for reply in packets:
+ print("Reply:\n")
+ reply.show()
+ icmp = reply.getlayer(sp.ICMPv6TimeExceeded)
+ if not icmp:
+ continue
+
+ ip = reply.getlayer(sp.IPv6)
+ assert icmp
+ assert ip.src == "64:ff9b::c000:202"
+ return reply
+
+ # If we don't find the packet we expect to see
+ assert False
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_source_addr_tcp(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \
+ / sp.TCP(sport=1111, dport=2222, flags="S")
+ self.common_test_source_addr(packet)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_source_addr_udp(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \
+ / sp.UDP(sport=1111, dport=2222) / sp.Raw("foo")
+ self.common_test_source_addr(packet)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_source_addr_sctp(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \
+ / sp.SCTP(sport=1111, dport=2222) \
+ / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500)
+ self.common_test_source_addr(packet)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_source_addr_icmp(self):
+ ToolsHelper.print_output("/sbin/route -6 add default 2001:db8::1")
+ import scapy.all as sp
+
+ packet = sp.IPv6(dst="64:ff9b::198.51.100.2", hlim=2) \
+ / sp.ICMPv6EchoRequest() / sp.Raw("foo")
+ reply = self.common_test_source_addr(packet)
+ icmp = reply.getlayer(sp.ICMPv6EchoRequest)
+ assert icmp
diff --git a/tests/sys/netpfil/pf/nat64.sh b/tests/sys/netpfil/pf/nat64.sh
new file mode 100644
index 000000000000..0bba1470c4c5
--- /dev/null
+++ b/tests/sys/netpfil/pf/nat64.sh
@@ -0,0 +1,1056 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+nat64_setup_base()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.1
+
+ jexec rtr pfctl -e
+}
+
+nat64_setup_in()
+{
+ nat64_setup_base
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)"
+}
+
+nat64_setup_out()
+{
+ nat64_setup_base
+ jexec rtr sysctl net.inet6.ip6.forwarding=1
+ # AF translation happens post-routing, traffic must be directed
+ # towards the outbound interface using routes for the original AF.
+ # jexec rtr ifconfig ${epair_link}a inet6 2001:db8:2::1/64 up no_dad
+ jexec rtr route add -inet6 64:ff9b::/96 -iface ${epair_link}a;
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass quick inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in quick on ${epair}b from any to 64:ff9b::/96" \
+ "pass out quick on ${epair_link}a from any to 64:ff9b::/96 af-to inet from (${epair_link}a)" \
+ "block"
+}
+
+atf_test_case "icmp_echo_in" "cleanup"
+icmp_echo_in_head()
+{
+ atf_set descr 'Basic NAT64 ICMP echo test on inbound interface'
+ atf_set require.user root
+}
+
+icmp_echo_in_body()
+{
+ nat64_setup_in
+
+ # One ping
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ # Make sure packets make it even when state is established
+ atf_check -s exit:0 \
+ -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \
+ ping6 -c 5 64:ff9b::192.0.2.2
+}
+
+icmp_echo_in_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "icmp_echo_out" "cleanup"
+icmp_echo_out_head()
+{
+ atf_set descr 'Basic NAT64 ICMP echo test on outbound interface'
+ atf_set require.user root
+}
+
+icmp_echo_out_body()
+{
+ nat64_setup_out
+
+ # One ping
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ # Make sure packets make it even when state is established
+ atf_check -s exit:0 \
+ -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \
+ ping6 -c 5 64:ff9b::192.0.2.2
+}
+
+icmp_echo_out_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "fragmentation_in" "cleanup"
+fragmentation_in_head()
+{
+ atf_set descr 'Test fragmented packets on inbound interface'
+ atf_set require.user root
+}
+
+fragmentation_in_body()
+{
+ nat64_setup_in
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 -s 1280 64:ff9b::192.0.2.2
+
+ atf_check -s exit:0 \
+ -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \
+ ping6 -c 3 -s 2000 64:ff9b::192.0.2.2
+ atf_check -s exit:0 \
+ -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \
+ ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2
+}
+
+fragmentation_in_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "fragmentation_out" "cleanup"
+fragmentation_out_head()
+{
+ atf_set descr 'Test fragmented packets on outbound interface'
+ atf_set require.user root
+}
+
+fragmentation_out_body()
+{
+ nat64_setup_out
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 -s 1280 64:ff9b::192.0.2.2
+
+ atf_check -s exit:0 \
+ -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \
+ ping6 -c 3 -s 2000 64:ff9b::192.0.2.2
+ atf_check -s exit:0 \
+ -o match:'3 packets transmitted, 3 packets received, 0.0% packet loss' \
+ ping6 -c 3 -s 10000 -b 20000 64:ff9b::192.0.2.2
+}
+
+fragmentation_out_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "tcp_in" "cleanup"
+tcp_in_head()
+{
+ atf_set descr 'TCP NAT64 test on inbound interface'
+ atf_set require.user root
+}
+
+tcp_in_body()
+{
+ nat64_setup_in
+
+ echo "foo" | jexec dst nc -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to TCP server"
+ fi
+}
+
+tcp_in_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "tcp_out" "cleanup"
+tcp_out_head()
+{
+ atf_set descr 'TCP NAT64 test on outbound interface'
+ atf_set require.user root
+}
+
+tcp_out_body()
+{
+ nat64_setup_out
+
+ echo "foo" | jexec dst nc -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(nc -w 3 -6 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to TCP server"
+ fi
+}
+
+tcp_out_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "udp_in" "cleanup"
+udp_in_head()
+{
+ atf_set descr 'UDP NAT64 test on inbound interface'
+ atf_set require.user root
+}
+
+udp_in_body()
+{
+ nat64_setup_in
+
+ echo "foo" | jexec dst nc -u -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to UDP server"
+ fi
+}
+
+udp_in_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "udp_out" "cleanup"
+udp_out_head()
+{
+ atf_set descr 'UDP NAT64 test on outbound interface'
+ atf_set require.user root
+}
+
+udp_out_body()
+{
+ nat64_setup_out
+
+ echo "foo" | jexec dst nc -u -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(echo bar | nc -w 3 -6 -u 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to UDP server"
+ fi
+}
+
+udp_out_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "sctp_in" "cleanup"
+sctp_in_head()
+{
+ atf_set descr 'SCTP NAT64 test on inbound interface'
+ atf_set require.user root
+}
+
+sctp_in_body()
+{
+ nat64_setup_in
+ if ! kldstat -q -m sctp; then
+ atf_skip "This test requires SCTP"
+ fi
+
+ echo "foo" | jexec dst nc --sctp -N -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to SCTP server"
+ fi
+}
+
+sctp_in_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "sctp_out" "cleanup"
+sctp_out_head()
+{
+ atf_set descr 'SCTP NAT64 test on outbound interface'
+ atf_set require.user root
+}
+
+sctp_out_body()
+{
+ nat64_setup_out
+ if ! kldstat -q -m sctp; then
+ atf_skip "This test requires SCTP"
+ fi
+
+ echo "foo" | jexec dst nc --sctp -N -l 1234 &
+
+ # Sanity check & delay for nc startup
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ rcv=$(echo bar | nc --sctp -w 3 -6 64:ff9b::c000:202 1234)
+ if [ "${rcv}" != "foo" ];
+ then
+ echo "rcv=${rcv}"
+ atf_fail "Failed to connect to SCTP server"
+ fi
+}
+
+sctp_out_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "tos" "cleanup"
+tos_head()
+{
+ atf_set descr 'ToS translation test'
+ atf_set require.user root
+}
+
+tos_body()
+{
+ nat64_setup_in
+
+ # Ensure we can distinguish ToS on the destination
+ jexec dst pfctl -e
+ pft_set_rules dst \
+ "pass" \
+ "block in inet tos 8"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 -z 4 64:ff9b::192.0.2.2
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 1 -z 8 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 -z 16 64:ff9b::192.0.2.2
+
+ jexec dst pfctl -sr -vv
+}
+
+tos_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "no_v4" "cleanup"
+no_v4_head()
+{
+ atf_set descr 'Test error handling when there is no IPv4 address to translate to'
+ atf_set require.user root
+}
+
+no_v4_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)"
+
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 3 64:ff9b::192.0.2.2
+}
+
+no_v4_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "range" "cleanup"
+range_head()
+{
+ atf_set descr 'Test using an address range for the IPv4 side'
+ atf_set require.user root
+}
+
+range_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up
+ jexec dst route add default 192.0.2.2
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ jexec rtr ping -c 1 192.0.2.254
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.3
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.2/31 round-robin"
+
+ # Use pf to count sources
+ jexec dst pfctl -e
+ pft_set_rules dst \
+ "pass"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.254
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.254
+
+ # Verify on dst that we saw different source addresses
+ atf_check -s exit:0 -o match:".*192.0.2.2.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.3.*" \
+ jexec dst pfctl -ss
+}
+
+range_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "pool" "cleanup"
+pool_head()
+{
+ atf_set descr 'Use a pool of IPv4 addresses'
+ atf_set require.user root
+}
+
+pool_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from { 192.0.2.1, 192.0.2.3, 192.0.2.4 } round-robin"
+
+ # Use pf to count sources
+ jexec dst pfctl -e
+ pft_set_rules dst \
+ "pass"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ # Verify on dst that we saw different source addresses
+ atf_check -s exit:0 -o match:".*192.0.2.1.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.3.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.4.*" \
+ jexec dst pfctl -ss
+}
+
+pool_cleanup()
+{
+ pft_cleanup
+}
+
+
+atf_test_case "table"
+table_head()
+{
+ atf_set descr 'Check table restrictions'
+ atf_set require.user root
+}
+
+table_body()
+{
+ pft_init
+
+ # Round-robin and random are allowed
+ echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> round-robin" | \
+ atf_check -s exit:0 \
+ pfctl -f -
+ echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> random" | \
+ atf_check -s exit:0 \
+ pfctl -f -
+
+ # bitmask is not
+ echo "pass in on epair inet6 from any to 64:ff9b::/96 af-to inet from <wanaddr> bitmask" | \
+ atf_check -s exit:1 \
+ -e match:"tables are not supported by pool type" \
+ pfctl -f -
+}
+
+table_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "table_range" "cleanup"
+table_range_head()
+{
+ atf_set descr 'Test using an address range within a table for the IPv4 side'
+ atf_set require.user root
+}
+
+table_range_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.2/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.254/24 up
+ jexec dst route add default 192.0.2.2
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.2
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "table <wanaddrs> { 192.0.2.2/31 }" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> round-robin"
+
+ # Use pf to count sources
+ jexec dst pfctl -e
+ pft_set_rules dst \
+ "pass"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.254
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.254
+
+ # Verify on dst that we saw different source addresses
+ atf_check -s exit:0 -o match:".*192.0.2.2.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.3.*" \
+ jexec dst pfctl -ss
+}
+
+table_range_cleanup()
+{
+ pft_cleanup
+}
+
+table_common_body()
+{
+ pool_type=$1
+
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.3/24 up
+ jexec rtr ifconfig ${epair_link}a inet alias 192.0.2.4/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "table <wanaddrs> { 192.0.2.1, 192.0.2.3, 192.0.2.4 }" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 af-to inet from <wanaddrs> ${pool_type}"
+
+ # Use pf to count sources
+ jexec dst pfctl -e
+ pft_set_rules dst \
+ "pass"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ # XXX We can't reasonably check pool type random because it's random. It may end
+ # up choosing the same source IP for all three connections.
+ if [ "${pool_type}" == "round-robin" ];
+ then
+ # Verify on dst that we saw different source addresses
+ atf_check -s exit:0 -o match:".*192.0.2.1.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.3.*" \
+ jexec dst pfctl -ss
+ atf_check -s exit:0 -o match:".*192.0.2.4.*" \
+ jexec dst pfctl -ss
+ fi
+}
+
+atf_test_case "table_round_robin" "cleanup"
+table_round_robin_head()
+{
+ atf_set descr 'Use a table of IPv4 addresses in round-robin mode'
+ atf_set require.user root
+}
+
+table_round_robin_body()
+{
+ table_common_body round-robin
+}
+
+table_round_robin_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "table_random" "cleanup"
+table_random_head()
+{
+ atf_set descr 'Use a table of IPv4 addresses in random mode'
+ atf_set require.user root
+}
+
+table_random_body()
+{
+ table_common_body random
+}
+
+table_random_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "dummynet" "cleanup"
+dummynet_head()
+{
+ atf_set descr 'Test dummynet on af-to rules'
+ atf_set require.user root
+}
+
+dummynet_body()
+{
+ pft_init
+ dummynet_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.1
+
+ jexec rtr pfctl -e
+ jexec rtr dnctl pipe 1 config delay 600
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b inet6 from any to 64:ff9b::/96 dnpipe 1 af-to inet from (${epair_link}a)"
+
+ # The ping request will pass, but take 1.2 seconds (.6 in, .6 out)
+ # So this works:
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 -t 2 64:ff9b::192.0.2.2
+
+ # But this times out:
+ atf_check -s exit:2 -o ignore \
+ ping6 -c 1 -t 1 64:ff9b::192.0.2.2
+}
+
+dummynet_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "gateway6" "cleanup"
+gateway6_head()
+{
+ atf_set descr 'NAT64 with a routing hop on the v6 side'
+ atf_set require.user root
+}
+
+gateway6_body()
+{
+ pft_init
+
+ epair_lan_link=$(vnet_mkepair)
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8:1::2/64 up no_dad
+ route -6 add default 2001:db8:1::1
+
+ vnet_mkjail lan_rtr ${epair}b ${epair_lan_link}a
+ jexec lan_rtr ifconfig ${epair}b inet6 2001:db8:1::1/64 up no_dad
+ jexec lan_rtr ifconfig ${epair_lan_link}a inet6 2001:db8::2/64 up no_dad
+ jexec lan_rtr route -6 add default 2001:db8::1
+ jexec lan_rtr sysctl net.inet6.ip6.forwarding=1
+
+ vnet_mkjail rtr ${epair_lan_link}b ${epair_link}a
+ jexec rtr ifconfig ${epair_lan_link}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+ jexec rtr route -6 add default 2001:db8::2
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8:1::1
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec dst ping -c 1 192.0.2.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair_lan_link}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)"
+
+ # One ping
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 64:ff9b::192.0.2.2
+
+ # Make sure packets make it even when state is established
+ atf_check -s exit:0 \
+ -o match:'5 packets transmitted, 5 packets received, 0.0% packet loss' \
+ ping6 -c 5 64:ff9b::192.0.2.2
+}
+
+gateway6_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "route_to" "cleanup"
+route_to_head()
+{
+ atf_set descr 'Test route-to on af-to rules'
+ atf_set require.user root
+}
+
+route_to_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair_null=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a ${epair_null}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_null}a 192.0.2.3/24 up
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b route-to (${epair_link}a 192.0.2.2) inet6 from any to 64:ff9b::/96 af-to inet from (${epair_link}a)"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 3 64:ff9b::192.0.2.2
+}
+
+route_to_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "reply_to" "cleanup"
+reply_to_head()
+{
+ atf_set descr 'Test reply-to on af-to rules'
+ atf_set require.user root
+}
+
+reply_to_body()
+{
+ pft_init
+
+ epair_link=$(vnet_mkepair)
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair}b ${epair_link}a
+ jexec rtr ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_link}a 192.0.2.1/24 up
+
+ vnet_mkjail dst ${epair_link}b
+ jexec dst ifconfig ${epair_link}b 192.0.2.2/24 up
+ jexec dst route add default 192.0.2.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair}b reply-to (${epair}b 2001:db8::2) inet6 from any to 64:ff9b::/96 af-to inet from 192.0.2.1"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 3 64:ff9b::192.0.2.2
+}
+
+reply_to_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "v6_gateway" "cleanup"
+v6_gateway_head()
+{
+ atf_set descr 'nat64 when the IPv4 gateway is given by an IPv6 address'
+ atf_set require.user root
+}
+
+v6_gateway_body()
+{
+ pft_init
+
+ epair_wan_two=$(vnet_mkepair)
+ epair_wan_one=$(vnet_mkepair)
+ epair_lan=$(vnet_mkepair)
+
+ ifconfig ${epair_lan}a inet6 2001:db8::2/64 up no_dad
+ route -6 add default 2001:db8::1
+
+ vnet_mkjail rtr ${epair_lan}b ${epair_wan_one}a
+ jexec rtr ifconfig ${epair_lan}b inet6 2001:db8::1/64 up no_dad
+ jexec rtr ifconfig ${epair_wan_one}a 192.0.2.1/24 up
+ jexec rtr ifconfig ${epair_wan_one}a inet6 -ifdisabled
+ jexec rtr route add default -inet6 fe80::1%${epair_wan_one}a
+ #jexec rtr route add default 192.0.2.2
+
+ vnet_mkjail wan_one ${epair_wan_one}b ${epair_wan_two}a
+ jexec wan_one ifconfig ${epair_wan_one}b 192.0.2.2/24 up
+ jexec wan_one ifconfig ${epair_wan_one}b inet6 fe80::1/64
+ jexec wan_one ifconfig ${epair_wan_two}a 198.51.100.2/24 up
+ jexec wan_one route add default 192.0.2.1
+ jexec wan_one sysctl net.inet.ip.forwarding=1
+
+ vnet_mkjail wan_two ${epair_wan_two}b
+ jexec wan_two ifconfig ${epair_wan_two}b 198.51.100.1/24 up
+ jexec wan_two route add default 198.51.100.2
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 1 2001:db8::1
+ atf_check -s exit:0 -o ignore \
+ jexec rtr ping -c 1 192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ jexec rtr ping -c 1 198.51.100.1
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "set reassemble yes" \
+ "set state-policy if-bound" \
+ "pass in on ${epair_lan}b inet6 from any to 64:ff9b::/96 af-to inet from (${epair_wan_one}a)"
+
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 3 64:ff9b::192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping6 -c 3 64:ff9b::198.51.100.1
+}
+
+v6_gateway_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "icmp_echo_in"
+ atf_add_test_case "icmp_echo_out"
+ atf_add_test_case "fragmentation_in"
+ atf_add_test_case "fragmentation_out"
+ atf_add_test_case "tcp_in"
+ atf_add_test_case "tcp_out"
+ atf_add_test_case "udp_in"
+ atf_add_test_case "udp_out"
+ atf_add_test_case "sctp_in"
+ atf_add_test_case "sctp_out"
+ atf_add_test_case "tos"
+ atf_add_test_case "no_v4"
+ atf_add_test_case "range"
+ atf_add_test_case "pool"
+ atf_add_test_case "table"
+ atf_add_test_case "table_range"
+ atf_add_test_case "table_round_robin"
+ atf_add_test_case "table_random"
+ atf_add_test_case "dummynet"
+ atf_add_test_case "gateway6"
+ atf_add_test_case "route_to"
+ atf_add_test_case "reply_to"
+ atf_add_test_case "v6_gateway"
+}
diff --git a/tests/sys/netpfil/pf/nat66.py b/tests/sys/netpfil/pf/nat66.py
index 3a037ac710fc..16b4ef3dd02b 100644
--- a/tests/sys/netpfil/pf/nat66.py
+++ b/tests/sys/netpfil/pf/nat66.py
@@ -29,23 +29,10 @@ import ipaddress
import pytest
import re
import socket
-import threading
-import time
+from utils import DelayedSend
from atf_python.sys.net.tools import ToolsHelper
from atf_python.sys.net.vnet import VnetTestTemplate
-class DelayedSend(threading.Thread):
- def __init__(self, packet):
- threading.Thread.__init__(self)
- self._packet = packet
-
- self.start()
-
- def run(self):
- import scapy.all as sp
- time.sleep(1)
- sp.send(self._packet)
-
class TestNAT66(VnetTestTemplate):
REQUIRED_MODULES = [ "pf" ]
TOPOLOGY = {
@@ -140,6 +127,7 @@ class TestNAT66(VnetTestTemplate):
assert found
@pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
def test_npt_icmp(self):
cl_vnet = self.vnet_map["vnet1"]
ifname = cl_vnet.iface_alias_map["if1"].name
@@ -168,6 +156,7 @@ class TestNAT66(VnetTestTemplate):
self.check_icmp_too_big(sp, 12000, 5000)
@pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
def test_npt_route_to_icmp(self):
cl_vnet = self.vnet_map["vnet1"]
ifname = cl_vnet.iface_alias_map["if1"].name
diff --git a/tests/sys/netpfil/pf/pass_block.sh b/tests/sys/netpfil/pf/pass_block.sh
index faf5c2de4de2..e955068d014b 100644
--- a/tests/sys/netpfil/pf/pass_block.sh
+++ b/tests/sys/netpfil/pf/pass_block.sh
@@ -232,7 +232,7 @@ urpf_head()
{
atf_set descr "Test unicast reverse path forwarding check"
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
urpf_body()
@@ -292,6 +292,165 @@ urpf_cleanup()
pft_cleanup
}
+atf_test_case "received_on" "cleanup"
+received_on_head()
+{
+ atf_set descr 'Test received-on filtering'
+ atf_set require.user root
+}
+
+received_on_body()
+{
+ pft_init
+
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+ epair_route=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair_one}b ${epair_two}b ${epair_route}a
+ vnet_mkjail srv ${epair_route}b
+
+ ifconfig ${epair_one}a 192.0.2.2/24 up
+ ifconfig ${epair_two}a 198.51.100.2/24 up
+ route add 203.0.113.2 192.0.2.1
+ route add 203.0.113.3 198.51.100.1
+
+ jexec alcatraz ifconfig ${epair_one}b 192.0.2.1/24 up
+ jexec alcatraz ifconfig ${epair_two}b 198.51.100.1/24 up
+ jexec alcatraz ifconfig ${epair_route}a 203.0.113.1/24 up
+ jexec alcatraz sysctl net.inet.ip.forwarding=1
+
+ jexec srv ifconfig ${epair_route}b 203.0.113.2/24 up
+ jexec srv ifconfig ${epair_route}b inet alias 203.0.113.3/24 up
+ jexec srv route add default 203.0.113.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 198.51.100.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.2
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.3
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block in" \
+ "pass received-on ${epair_one}b"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 198.51.100.1
+
+ # And ensure we can check the received-on interface after routing
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.2
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 203.0.113.3
+
+ # Now try this with a group instead
+ jexec alcatraz ifconfig ${epair_one}b group test
+ pft_set_rules alcatraz \
+ "block in" \
+ "pass received-on test"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 198.51.100.1
+
+ # And ensure we can check the received-on interface after routing
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.2
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 203.0.113.3
+
+ # Test '! received-on'
+ pft_set_rules alcatraz \
+ "pass in" \
+ "block ! received-on ${epair_one}b"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 198.51.100.1
+}
+
+received_on_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "optimize_any" "cleanup"
+optimize_any_head()
+{
+ atf_set descr 'Test known optimizer bug'
+ atf_set require.user root
+}
+
+optimize_any_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair}a
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block" \
+ "pass in inet from { any, 192.0.2.3 }"
+
+ atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1
+}
+
+optimize_any_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "any_if" "cleanup"
+any_if_head()
+{
+ atf_set descr 'Test the any interface keyword'
+ atf_set require.user root
+}
+
+any_if_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "block" \
+ "pass in on any"
+
+ atf_check -s exit:0 -o ignore ping -c 1 -t 1 192.0.2.1
+}
+
+any_if_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "enable_disable"
@@ -300,4 +459,7 @@ atf_init_test_cases()
atf_add_test_case "noalias"
atf_add_test_case "nested_inline"
atf_add_test_case "urpf"
+ atf_add_test_case "received_on"
+ atf_add_test_case "optimize_any"
+ atf_add_test_case "any_if"
}
diff --git a/tests/sys/netpfil/pf/pflog.sh b/tests/sys/netpfil/pf/pflog.sh
index 75b7c5c217bb..a34ec893a75c 100644
--- a/tests/sys/netpfil/pf/pflog.sh
+++ b/tests/sys/netpfil/pf/pflog.sh
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2023 Rubicon Communications, LLC (Netgate)
+# Copyright (c) 2024 Deciso B.V.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
@@ -33,7 +34,7 @@ malformed_head()
{
atf_set descr 'Test that we do not log malformed packets as passing'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
malformed_body()
@@ -79,7 +80,327 @@ malformed_cleanup()
pft_cleanup
}
+atf_test_case "matches" "cleanup"
+matches_head()
+{
+ atf_set descr 'Test the pflog matches keyword'
+ atf_set require.user root
+}
+
+matches_body()
+{
+ pflog_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz ifconfig pflog0 up
+ pft_set_rules alcatraz \
+ "match log(matches) inet proto icmp" \
+ "match log(matches) inet from 192.0.2.2" \
+ "pass"
+
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ echo "Rules"
+ jexec alcatraz pfctl -sr -vv
+ echo "States"
+ jexec alcatraz pfctl -ss -vv
+ echo "Log"
+ cat ${PWD}/pflog.txt
+
+ atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog.txt
+ atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog.txt
+}
+
+matches_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "matches_logif" "cleanup"
+matches_logif_head()
+{
+ atf_set descr 'Test log(matches, to pflogX)'
+ atf_set require.user root
+}
+
+matches_logif_body()
+{
+ pflog_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz ifconfig pflog0 up
+ jexec alcatraz ifconfig pflog1 create
+ jexec alcatraz ifconfig pflog1 up
+ pft_set_rules alcatraz \
+ "match log(matches, to pflog1) inet proto icmp" \
+ "match log inet from 192.0.2.2" \
+ "pass log(to pflog0)"
+
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog1 >> ${PWD}/pflog1.txt &
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog0.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ echo "Rules"
+ jexec alcatraz pfctl -sr -vv
+ echo "States"
+ jexec alcatraz pfctl -ss -vv
+ echo "Log 0"
+ cat ${PWD}/pflog0.txt
+ echo "Log 1"
+ cat ${PWD}/pflog1.txt
+
+ atf_check -o match:".*rule 0/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog1.txt
+ atf_check -o match:".*rule 1/0\(match\): match in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog1.txt
+}
+
+matches_logif_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "state_max" "cleanup"
+state_max_head()
+{
+ atf_set descr 'Ensure that drops due to state limits are logged'
+ atf_set require.user root
+}
+
+state_max_body()
+{
+ pflog_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz ifconfig pflog0 up
+ pft_set_rules alcatraz "pass log inet keep state (max 1)"
+
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 192.0.2.1
+
+ echo "Rules"
+ jexec alcatraz pfctl -sr -vv
+ echo "States"
+ jexec alcatraz pfctl -ss -vv
+ echo "Log"
+ cat ${PWD}/pflog.txt
+
+ # First ping passes.
+ atf_check -o match:".*rule 0/0\(match\): pass in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog.txt
+
+ # Second ping is blocked due to the state limit.
+ atf_check -o match:".*rule 0/12\(state-limit\): block in on ${epair}a: 192.0.2.2 > 192.0.2.1: ICMP echo request.*" \
+ cat pflog.txt
+
+ # At most three lines should be written: one for the first ping, and
+ # two for the second: one for the initial pass through the ruleset, and
+ # then a drop because of the state limit. Ideally only the drop would
+ # be logged; if this is fixed, the count will be 2 instead of 3.
+ atf_check -o match:3 grep -c . pflog.txt
+
+ # If the rule doesn't specify logging, we shouldn't log drops
+ # due to state limits.
+ pft_set_rules alcatraz "pass inet keep state (max 1)"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ atf_check -s exit:2 -o ignore \
+ ping -c 1 192.0.2.1
+
+ atf_check -o match:3 grep -c . pflog.txt
+}
+
+state_max_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "unspecified_v4" "cleanup"
+unspecified_v4_head()
+{
+ atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks'
+ atf_set require.user root
+}
+
+unspecified_v4_body()
+{
+ pflog_init
+
+ vnet_mkjail alcatraz
+ jexec alcatraz ifconfig lo0 inet 127.0.0.1
+ jexec alcatraz route add default 127.0.0.1
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz ifconfig pflog0 up
+ pft_set_rules alcatraz "block log on lo0 to 0.0.0.0"
+
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ atf_check -s not-exit:0 -o ignore -e ignore \
+ jexec alcatraz ping -S 127.0.0.1 -c 1 0.0.0.0
+
+ atf_check -o match:".*: block out on lo0: 127.0.0.1 > 0.0.0.0: ICMP echo request,.*" \
+ cat pflog.txt
+}
+
+unspecified_v4_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "unspecified_v6" "cleanup"
+unspecified_v6_head()
+{
+ atf_set descr 'Ensure that packets to the unspecified address are visible to pfil hooks'
+ atf_set require.user root
+}
+
+unspecified_v6_body()
+{
+ pflog_init
+
+ vnet_mkjail alcatraz
+ jexec alcatraz ifconfig lo0 up
+ jexec alcatraz route -6 add ::0 ::1
+
+ jexec alcatraz pfctl -e
+ jexec alcatraz ifconfig pflog0 up
+ pft_set_rules alcatraz "block log on lo0 to ::0"
+
+ jexec alcatraz tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> pflog.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ atf_check -s not-exit:0 -o ignore -e ignore \
+ jexec alcatraz ping -6 -S ::1 -c 1 ::0
+
+ cat pflog.txt
+ atf_check -o match:".*: block out on lo0: ::1 > ::: ICMP6, echo request,.*" \
+ cat pflog.txt
+}
+
+unspecified_v6_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "rdr_action" "cleanup"
+rdr_head()
+{
+ atf_set descr 'Ensure that NAT rule actions are logged correctly'
+ atf_set require.user root
+}
+
+rdr_action_body()
+{
+ pflog_init
+
+ j="pflog:rdr_action"
+ epair_c=$(vnet_mkepair)
+ epair_srv=$(vnet_mkepair)
+
+ vnet_mkjail ${j}srv ${epair_srv}a
+ vnet_mkjail ${j}gw ${epair_srv}b ${epair_c}a
+ vnet_mkjail ${j}c ${epair_c}b
+
+ jexec ${j}srv ifconfig ${epair_srv}a 198.51.100.1/24 up
+ # No default route in srv jail, to ensure we're NAT-ing
+ jexec ${j}gw ifconfig ${epair_srv}b 198.51.100.2/24 up
+ jexec ${j}gw ifconfig ${epair_c}a 192.0.2.1/24 up
+ jexec ${j}gw sysctl net.inet.ip.forwarding=1
+ jexec ${j}c ifconfig ${epair_c}b 192.0.2.2/24 up
+ jexec ${j}c route add default 192.0.2.1
+
+ jexec ${j}gw pfctl -e
+ jexec ${j}gw ifconfig pflog0 up
+ pft_set_rules ${j}gw \
+ "rdr log on ${epair_srv}b proto tcp from 198.51.100.0/24 to any port 1234 -> 192.0.2.2 port 1234" \
+ "block quick inet6" \
+ "pass in log"
+
+ jexec ${j}gw tcpdump -n -e -ttt --immediate-mode -l -U -i pflog0 >> ${PWD}/pflog.txt &
+ sleep 1 # Wait for tcpdump to start
+
+ # send a SYN to catch in the log
+ jexec ${j}srv nc -N -w 0 198.51.100.2 1234
+
+ echo "Log"
+ cat ${PWD}/pflog.txt
+
+ # log line generated for rdr hit (pre-NAT)
+ atf_check -o match:".*.*rule 0/0\(match\): rdr in on ${epair_srv}b: 198.51.100.1.[0-9]* > 198.51.100.2.1234: Flags \[S\].*" \
+ cat pflog.txt
+
+ # log line generated for pass hit (post-NAT)
+ atf_check -o match:".*.*rule 1/0\(match\): pass in on ${epair_srv}b: 198.51.100.1.[0-9]* > 192.0.2.2.1234: Flags \[S\].*" \
+ cat pflog.txt
+
+ # only two log lines shall be written
+ atf_check -o match:2 grep -c . pflog.txt
+}
+
+rdr_action_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "malformed"
+ atf_add_test_case "matches"
+ atf_add_test_case "matches_logif"
+ atf_add_test_case "state_max"
+ atf_add_test_case "unspecified_v4"
+ atf_add_test_case "unspecified_v6"
+ atf_add_test_case "rdr_action"
}
diff --git a/tests/sys/netpfil/pf/pflow.sh b/tests/sys/netpfil/pf/pflow.sh
index f0552eb061da..1122096d2e31 100644
--- a/tests/sys/netpfil/pf/pflow.sh
+++ b/tests/sys/netpfil/pf/pflow.sh
@@ -85,7 +85,7 @@ state_defaults_head()
{
atf_set descr 'Test set state-defaults pflow'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
state_defaults_body()
@@ -146,7 +146,7 @@ v6_head()
{
atf_set descr 'Test pflow over IPv6'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
@@ -188,7 +188,7 @@ nat_head()
{
atf_set descr 'Test pflow export for NAT44'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
nat_body()
@@ -239,7 +239,7 @@ rule_head()
{
atf_set descr 'Test per-rule pflow option'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
rule_body()
diff --git a/tests/sys/netpfil/pf/pfsync.sh b/tests/sys/netpfil/pf/pfsync.sh
index 87dfcf748d3c..7f545b43a066 100644
--- a/tests/sys/netpfil/pf/pfsync.sh
+++ b/tests/sys/netpfil/pf/pfsync.sh
@@ -126,7 +126,7 @@ defer_head()
{
atf_set descr 'Defer mode pfsync test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
defer_body()
@@ -335,6 +335,7 @@ pbr_common_body()
atf_skip "This test requires carp"
fi
pfsynct_init
+ vnet_init_bridge
bridge0=$(vnet_mkbridge)
bridge1=$(vnet_mkbridge)
@@ -834,16 +835,11 @@ basic_ipv6_cleanup()
pfsynct_cleanup
}
-atf_test_case "route_to" "cleanup"
-route_to_head()
+route_to_common_head()
{
- atf_set descr 'Test route-to with default rule'
- atf_set require.user root
- atf_set require.progs scapy
-}
+ pfsync_version=$1
+ shift
-route_to_body()
-{
pfsynct_init
epair_sync=$(vnet_mkepair)
@@ -865,40 +861,111 @@ route_to_body()
jexec one ifconfig pfsync0 \
syncdev ${epair_sync}a \
maxupd 1 \
+ version $pfsync_version \
up
jexec two ifconfig ${epair_sync}b 192.0.2.2/24 up
jexec two ifconfig ${epair_two}a 198.51.100.2/24 up
jexec two ifconfig ${epair_out_two}a 203.0.113.2/24 up
- #jexec two ifconfig ${epair_out_two}a name outif
+ jexec two ifconfig ${epair_out_two}a name outif
jexec two sysctl net.inet.ip.forwarding=1
jexec two arp -s 203.0.113.254 00:01:02:03:04:05
jexec two ifconfig pfsync0 \
syncdev ${epair_sync}b \
maxupd 1 \
+ version $pfsync_version \
up
- # Enable pf!
+ ifconfig ${epair_one}b 198.51.100.254/24 up
+ ifconfig ${epair_two}b 198.51.100.253/24 up
+ route add -net 203.0.113.0/24 198.51.100.1
+ ifconfig ${epair_two}b up
+ ifconfig ${epair_out_one}b up
+ ifconfig ${epair_out_two}b up
+}
+
+route_to_common_tail()
+{
+ atf_check -s exit:0 env PYTHONPATH=${common_dir} \
+ ${common_dir}/pft_ping.py \
+ --sendif ${epair_one}b \
+ --fromaddr 198.51.100.254 \
+ --to 203.0.113.254 \
+ --recvif ${epair_out_one}b
+
+ # Allow time for sync
+ sleep 2
+
+ states_one=$(mktemp)
+ states_two=$(mktemp)
+ jexec one pfctl -qvvss | normalize_pfctl_s > $states_one
+ jexec two pfctl -qvvss | normalize_pfctl_s > $states_two
+}
+
+atf_test_case "route_to_1301_body" "cleanup"
+route_to_1301_head()
+{
+ atf_set descr 'Test route-to with pfsync version 13.1'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+route_to_1301_body()
+{
+ route_to_common_head 1301
+
jexec one pfctl -e
pft_set_rules one \
"set skip on ${epair_sync}a" \
"pass out route-to (outif 203.0.113.254)"
+
jexec two pfctl -e
+ pft_set_rules two \
+ "set skip on ${epair_sync}b" \
+ "pass out route-to (outif 203.0.113.254)"
+
+ route_to_common_tail
- # Make sure we have different rulesets so the synced state is associated with
- # V_pf_default_rule
+ # Sanity check
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one ||
+ atf_fail "State missing on router one"
+
+ # With identical ruleset the routing information is recovered from the matching rule.
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif' $states_two ||
+ atf_fail "State missing on router two"
+
+ true
+}
+
+route_to_1301_cleanup()
+{
+ pfsynct_cleanup
+}
+
+atf_test_case "route_to_1301_bad_ruleset" "cleanup"
+route_to_1301_bad_ruleset_head()
+{
+ atf_set descr 'Test route-to with pfsync version 13.1 and incompatible ruleset'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+route_to_1301_bad_ruleset_body()
+{
+ route_to_common_head 1301
+
+ jexec one pfctl -e
+ pft_set_rules one \
+ "set skip on ${epair_sync}a" \
+ "pass out route-to (outif 203.0.113.254)"
+
+ jexec two pfctl -e
pft_set_rules two \
+ "set debug loud" \
"set skip on ${epair_sync}b" \
"pass out route-to (outif 203.0.113.254)" \
"pass out proto tcp"
- ifconfig ${epair_one}b 198.51.100.254/24 up
- ifconfig ${epair_two}b 198.51.100.253/24 up
- route add -net 203.0.113.0/24 198.51.100.1
- ifconfig ${epair_two}b up
- ifconfig ${epair_out_one}b up
- ifconfig ${epair_out_two}b up
-
atf_check -s exit:0 env PYTHONPATH=${common_dir} \
${common_dir}/pft_ping.py \
--sendif ${epair_one}b \
@@ -906,25 +973,151 @@ route_to_body()
--to 203.0.113.254 \
--recvif ${epair_out_one}b
- # Allow time for sync
- ifconfig ${epair_one}b inet 198.51.100.254 -alias
- route del -net 203.0.113.0/24 198.51.100.1
- route add -net 203.0.113.0/24 198.51.100.2
+ route_to_common_tail
- sleep 2
+ # Sanity check
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one ||
+ atf_fail "State missing on router one"
- # Now try to trigger the state on the other pfsync member
- env PYTHONPATH=${common_dir} \
+ # Different ruleset on each router means the routing information recovery
+ # from rule is impossible. The state is not synced.
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two &&
+ atf_fail "State present on router two"
+
+ true
+}
+
+route_to_1301_bad_ruleset_cleanup()
+{
+ pfsynct_cleanup
+}
+
+atf_test_case "route_to_1301_bad_rpool" "cleanup"
+route_to_1301_bad_rpool_head()
+{
+ atf_set descr 'Test route-to with pfsync version 13.1 and different interface'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+route_to_1301_bad_rpool_body()
+{
+ route_to_common_head 1301
+
+ jexec one pfctl -e
+ pft_set_rules one \
+ "set skip on ${epair_sync}a" \
+ "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }"
+
+ jexec two pfctl -e
+ pft_set_rules two \
+ "set skip on ${epair_sync}b" \
+ "pass out route-to { (outif 203.0.113.254) (outif 203.0.113.254) }"
+
+ atf_check -s exit:0 env PYTHONPATH=${common_dir} \
${common_dir}/pft_ping.py \
- --sendif ${epair_two}b \
+ --sendif ${epair_one}b \
--fromaddr 198.51.100.254 \
--to 203.0.113.254 \
- --recvif ${epair_out_two}b
+ --recvif ${epair_out_one}b
+
+ route_to_common_tail
+
+ # Sanity check
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one ||
+ atf_fail "State missing on router one"
+
+ # The ruleset is identical but since the redirection pool contains multiple interfaces
+ # pfsync will not attempt to recover the routing information from the rule.
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two &&
+ atf_fail "State present on router two"
+
+ true
+}
+
+route_to_1301_bad_rpool_cleanup()
+{
+ pfsynct_cleanup
+}
+
+atf_test_case "route_to_1400_bad_ruleset" "cleanup"
+route_to_1400_bad_ruleset_head()
+{
+ atf_set descr 'Test route-to with pfsync version 14.0'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+route_to_1400_bad_ruleset_body()
+{
+ route_to_common_head 1400
+
+ jexec one pfctl -e
+ pft_set_rules one \
+ "set skip on ${epair_sync}a" \
+ "pass out route-to (outif 203.0.113.254)"
+
+ jexec two pfctl -e
+ pft_set_rules two \
+ "set skip on ${epair_sync}b"
+
+ route_to_common_tail
+
+ # Sanity check
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one ||
+ atf_fail "State missing on router one"
+
+ # Even with a different ruleset FreeBSD 14 syncs the state just fine.
+ # There's no recovery involved, the pfsync packet contains the routing information.
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .* route-to: 203.0.113.254@outif' $states_two ||
+ atf_fail "State missing on router two"
+
+ true
+}
+
+route_to_1400_bad_ruleset_cleanup()
+{
+ pfsynct_cleanup
+}
+
+atf_test_case "route_to_1400_bad_ifname" "cleanup"
+route_to_1400_bad_ifname_head()
+{
+ atf_set descr 'Test route-to with pfsync version 14.0'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+route_to_1400_bad_ifname_body()
+{
+ route_to_common_head 1400
+
+ jexec one pfctl -e
+ pft_set_rules one \
+ "set skip on ${epair_sync}a" \
+ "pass out route-to (outif 203.0.113.254)"
+
+ jexec two pfctl -e
+ jexec two ifconfig outif name outif_new
+ pft_set_rules two \
+ "set skip on ${epair_sync}b" \
+ "pass out route-to (outif_new 203.0.113.254)"
+
+ route_to_common_tail
+
+ # Sanity check
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*, rule 0 .* route-to: 203.0.113.254@outif origif: outif' $states_one ||
+ atf_fail "State missing on router one"
+
+ # Since FreeBSD 14 never attempts recovery of missing routing information
+ # a state synced to a router with a different interface name is dropped.
+ grep -qE 'all icmp 198.51.100.254 -> 203.0.113.254:8 .*' $states_two &&
+ atf_fail "State present on router two"
true
}
-route_to_cleanup()
+route_to_1400_bad_ifname_cleanup()
{
pfsynct_cleanup
}
@@ -941,5 +1134,9 @@ atf_init_test_cases()
atf_add_test_case "timeout"
atf_add_test_case "basic_ipv6_unicast"
atf_add_test_case "basic_ipv6"
- atf_add_test_case "route_to"
+ atf_add_test_case "route_to_1301"
+ atf_add_test_case "route_to_1301_bad_ruleset"
+ atf_add_test_case "route_to_1301_bad_rpool"
+ atf_add_test_case "route_to_1400_bad_ruleset"
+ atf_add_test_case "route_to_1400_bad_ifname"
}
diff --git a/tests/sys/netpfil/pf/pft_read_ipfix.py b/tests/sys/netpfil/pf/pft_read_ipfix.py
index 2c11bdfd130c..02ef2ca5ab06 100644
--- a/tests/sys/netpfil/pf/pft_read_ipfix.py
+++ b/tests/sys/netpfil/pf/pft_read_ipfix.py
@@ -60,7 +60,7 @@ def parse_ipfix(p):
c = datafl.payload
def receive(recvif, recvport):
- pkts = sp.sniff(iface=recvif, timeout=65)
+ pkts = sp.sniff(iface=recvif, timeout=65, filter="udp port 2055")
if len(pkts) == 0:
print("No data")
diff --git a/tests/sys/netpfil/pf/proxy.sh b/tests/sys/netpfil/pf/proxy.sh
index 4a7ea00a0cd4..78ce25930c04 100644
--- a/tests/sys/netpfil/pf/proxy.sh
+++ b/tests/sys/netpfil/pf/proxy.sh
@@ -57,7 +57,7 @@ ftp_body()
jexec srv route add default 198.51.100.1
# Start FTP server in srv
- jexec srv twistd ftp -r `pwd` -p 21
+ jexec srv twistd --logfile=/dev/null ftp -r `pwd` -p 21
# Sanity check
atf_check -s exit:0 -o ignore ping -c 1 198.51.100.2
@@ -74,7 +74,7 @@ ftp_body()
# Create a dummy file to download
echo 'foo' > remote.txt
- echo 'get remote.txt local.txt' | ftp -a 198.51.100.2
+ echo -e 'epsv\nget remote.txt local.txt' | ftp -a 198.51.100.2
# Compare the downloaded file to the original
if ! diff -q local.txt remote.txt;
diff --git a/tests/sys/netpfil/pf/rdr-srcport.py b/tests/sys/netpfil/pf/rdr-srcport.py
new file mode 100644
index 000000000000..633580582711
--- /dev/null
+++ b/tests/sys/netpfil/pf/rdr-srcport.py
@@ -0,0 +1,20 @@
+#
+# A helper script which accepts TCP connections and writes the remote port
+# number to the stream.
+#
+
+import socket
+
+def main():
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(('0.0.0.0', 8888))
+ s.listen(5)
+
+ while True:
+ cs, addr = s.accept()
+ cs.sendall(str(addr[1]).encode())
+ cs.close()
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/sys/netpfil/pf/rdr.sh b/tests/sys/netpfil/pf/rdr.sh
index 5e60b97c653b..4c08b4973891 100644
--- a/tests/sys/netpfil/pf/rdr.sh
+++ b/tests/sys/netpfil/pf/rdr.sh
@@ -27,14 +27,6 @@
. $(atf_get_srcdir)/utils.subr
-atf_test_case "tcp_v6" "cleanup"
-tcp_v6_head()
-{
- atf_set descr 'TCP rdr with IPv6'
- atf_set require.user root
- atf_set require.progs scapy python3
-}
-
#
# Test that rdr works for TCP with IPv6.
#
@@ -47,7 +39,7 @@ tcp_v6_head()
#
# Test for incorrect checksums after the rewrite by looking at a packet capture (see bug 210860)
#
-tcp_v6_body()
+tcp_v6_setup()
{
pft_init
@@ -83,16 +75,18 @@ tcp_v6_body()
jexec ${j}c route add -inet6 2001:db8:a::0/64 2001:db8:b::1
jexec ${j}b pfctl -e
+}
- pft_set_rules ${j}b \
- "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000"
+tcp_v6_common()
+{
+ pft_set_rules ${j}b "${1}"
# Check that a can reach c over the router
atf_check -s exit:0 -o ignore \
jexec ${j}a ping -6 -c 1 2001:db8:b::2
# capture packets on c so we can look for incorrect checksums
- jexec ${j}c tcpdump --immediate-mode -w ${j}.pcap tcp and port 8000 &
+ jexec ${j}c tcpdump --immediate-mode -w ${PWD}/${j}.pcap tcp and port 8000 &
tcpdumppid=$!
# start a web server and give it a second to start
@@ -112,16 +106,185 @@ tcp_v6_body()
# Check for 'incorrect' in packet capture, this should tell us if
# checksums are bad with rdr rules
- count=$(jexec ${j}c tcpdump -vvvv -r ${j}.pcap | grep incorrect | wc -l)
+ count=$(jexec ${j}c tcpdump -vvvv -r ${PWD}/${j}.pcap | grep incorrect | wc -l)
atf_check_equal " 0" "$count"
}
-tcp_v6_cleanup()
+atf_test_case "tcp_v6_compat" "cleanup"
+tcp_v6_compat_head()
+{
+ atf_set descr 'TCP rdr with IPv6 with NAT rules'
+ atf_set require.user root
+ atf_set require.progs python3
+}
+
+tcp_v6_compat_body()
+{
+ tcp_v6_setup # Sets ${epair_…} variables
+ tcp_v6_common \
+ "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000"
+}
+
+tcp_v6_compat_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "tcp_v6_pass" "cleanup"
+tcp_v6_pass_head()
+{
+ atf_set descr 'TCP rdr with IPv6 with pass/match rules'
+ atf_set require.user root
+ atf_set require.progs python3
+}
+
+tcp_v6_pass_body()
+{
+ tcp_v6_setup # Sets ${epair_…} variables
+ tcp_v6_common \
+ "rdr on ${epair_one}a proto tcp from any to any port 80 -> 2001:db8:b::2 port 8000"
+}
+
+tcp_v6_pass_cleanup()
+{
+ pft_cleanup
+}
+
+#
+# Test that rdr works for multiple TCP with same srcip and srcport.
+#
+# Four jails, a, b, c, d, are used:
+# - jail d runs a server on port 8888,
+# - jail a makes connections to the server, routed through jails b and c,
+# - jail b uses NAT to rewrite source addresses and ports to the same 2-tuple,
+# avoiding the need to use SO_REUSEADDR in jail a,
+# - jail c uses a redirect rule to map the destination address to the same
+# address and port, resulting in a NAT state conflict.
+#
+# In this case, the rdr rule should also rewrite the source port (again) to
+# resolve the state conflict.
+#
+srcport_setup()
+{
+ pft_init
+
+ j="rdr:srcport"
+ epair1=$(vnet_mkepair)
+ epair2=$(vnet_mkepair)
+ epair3=$(vnet_mkepair)
+
+ echo $epair_one
+ echo $epair_two
+
+ vnet_mkjail ${j}a ${epair1}a
+ vnet_mkjail ${j}b ${epair1}b ${epair2}a
+ vnet_mkjail ${j}c ${epair2}b ${epair3}a
+ vnet_mkjail ${j}d ${epair3}b
+
+ # configure addresses for a
+ jexec ${j}a ifconfig lo0 up
+ jexec ${j}a ifconfig ${epair1}a inet 198.51.100.50/24 up
+ jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.51/24
+ jexec ${j}a ifconfig ${epair1}a inet alias 198.51.100.52/24
+
+ # configure addresses for b
+ jexec ${j}b ifconfig lo0 up
+ jexec ${j}b ifconfig ${epair1}b inet 198.51.100.1/24 up
+ jexec ${j}b ifconfig ${epair2}a inet 198.51.101.2/24 up
+
+ # configure addresses for c
+ jexec ${j}c ifconfig lo0 up
+ jexec ${j}c ifconfig ${epair2}b inet 198.51.101.3/24 up
+ jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.4/24
+ jexec ${j}c ifconfig ${epair2}b inet alias 198.51.101.5/24
+ jexec ${j}c ifconfig ${epair3}a inet 203.0.113.1/24 up
+
+ # configure addresses for d
+ jexec ${j}d ifconfig lo0 up
+ jexec ${j}d ifconfig ${epair3}b inet 203.0.113.50/24 up
+
+ jexec ${j}b sysctl net.inet.ip.forwarding=1
+ jexec ${j}c sysctl net.inet.ip.forwarding=1
+ jexec ${j}b pfctl -e
+ jexec ${j}c pfctl -e
+}
+
+srcport_common()
+{
+ pft_set_rules ${j}b \
+ "set debug misc" \
+ "${1}"
+
+ pft_set_rules ${j}c \
+ "set debug misc" \
+ "${2}"
+
+ jexec ${j}a route add default 198.51.100.1
+ jexec ${j}c route add 198.51.100.0/24 198.51.101.2
+ jexec ${j}d route add 198.51.101.0/24 203.0.113.1
+
+ jexec ${j}d python3 $(atf_get_srcdir)/rdr-srcport.py &
+ sleep 1
+
+ echo a | jexec ${j}a nc -w 3 -s 198.51.100.50 -p 1234 198.51.101.3 7777 > port1
+
+ jexec ${j}a nc -s 198.51.100.51 -p 1234 198.51.101.4 7777 > port2 &
+ jexec ${j}a nc -s 198.51.100.52 -p 1234 198.51.101.5 7777 > port3 &
+ sleep 1
+
+ atf_check -o inline:"1234" cat port1
+ atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port2
+ atf_check -o match:"[0-9]+" -o not-inline:"1234" cat port3
+}
+
+atf_test_case "srcport_compat" "cleanup"
+srcport_compat_head()
+{
+ atf_set descr 'TCP rdr srcport modulation with NAT rules'
+ atf_set require.user root
+ atf_set require.progs python3
+ atf_set timeout 9999
+}
+
+srcport_compat_body()
+{
+ srcport_setup # Sets ${epair_…} variables
+ srcport_common \
+ "nat on ${epair2}a inet from 198.51.100.0/24 to any -> ${epair2}a static-port" \
+ "rdr on ${epair2}b proto tcp from any to ${epair2}b port 7777 -> 203.0.113.50 port 8888"
+}
+
+srcport_compat_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "srcport_pass" "cleanup"
+srcport_pass_head()
+{
+ atf_set descr 'TCP rdr srcport modulation with pass/match rules'
+ atf_set require.user root
+ atf_set require.progs python3
+ atf_set timeout 9999
+}
+
+srcport_pass_body()
+{
+ srcport_setup # Sets ${epair_…} variables
+ srcport_common \
+ "pass out on ${epair2}a inet from 198.51.100.0/24 to any nat-to ${epair2}a static-port" \
+ "pass in on ${epair2}b proto tcp from any to ${epair2}b port 7777 rdr-to 203.0.113.50 port 8888"
+}
+
+srcport_pass_cleanup()
{
pft_cleanup
}
atf_init_test_cases()
{
- atf_add_test_case "tcp_v6"
+ atf_add_test_case "tcp_v6_compat"
+ atf_add_test_case "tcp_v6_pass"
+ atf_add_test_case "srcport_compat"
+ atf_add_test_case "srcport_pass"
}
diff --git a/tests/sys/netpfil/pf/return.py b/tests/sys/netpfil/pf/return.py
new file mode 100644
index 000000000000..753012860764
--- /dev/null
+++ b/tests/sys/netpfil/pf/return.py
@@ -0,0 +1,153 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+import pytest
+import subprocess
+import re
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+def check(cmd):
+ ps = subprocess.Popen(cmd, shell=True)
+ ret = ps.wait()
+ if ret != 0:
+ raise Exception("Command %s returned %d" % (cmd, ret))
+
+class TestReturn(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1", "if2"]},
+ "vnet3": {"ifaces": ["if2"]},
+ "if1": {"prefixes4": [("192.0.2.2/24", "192.0.2.1/24")]},
+ "if2": {"prefixes4": [("198.51.100.1/24", "198.51.100.2/24")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ifname = vnet.iface_alias_map["if1"].name
+ if2name = vnet.iface_alias_map["if2"].name
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "nat on %s inet from 192.0.2.0/24 to any -> (%s)" % (ifname, ifname),
+ "block return",
+ "pass inet proto icmp icmp-type echoreq",
+ ])
+
+ ToolsHelper.print_output("/sbin/sysctl net.inet.ip.forwarding=1")
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+
+ def vnet3_handler(self, vnet):
+ ToolsHelper.print_output("/sbin/route add default 198.51.100.1")
+
+ def common_setup(self):
+ ToolsHelper.print_output("/sbin/route add default 192.0.2.1")
+
+ # Sanity check
+ check("/sbin/ping -c 1 192.0.2.1")
+ check("/sbin/ping -c 1 198.51.100.1")
+ check("/sbin/ping -c 2 198.51.100.2")
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_tcp(self):
+ self.common_setup()
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Send a TCP SYN, expect a RST
+ pkt = sp.IP(src="192.0.2.2", dst="198.51.100.2") \
+ / sp.TCP(sport=4321, dport=1234, flags="S")
+ print(pkt)
+ reply = sp.sr1(pkt, timeout=3, verbose=False)
+ print(reply)
+
+ ip = reply.getlayer(sp.IP)
+ tcp = reply.getlayer(sp.TCP)
+ assert ip
+ assert ip.src == "198.51.100.2"
+ assert ip.dst == "192.0.2.2"
+ assert tcp
+ assert tcp.sport == 1234
+ assert tcp.dport == 4321
+ assert "R" in tcp.flags
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_udp(self):
+ self.common_setup()
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Send a UDP packet, expect ICMP error
+ pkt = sp.IP(dst="198.51.100.2") \
+ / sp.UDP(sport=4321, dport=1234)
+ print(pkt)
+ reply = sp.sr1(pkt, timeout=3, verbose=False)
+ print(reply)
+ ip = reply.getlayer(sp.IP)
+ icmp = reply.getlayer(sp.ICMP)
+ udp = reply.getlayer(sp.UDPerror)
+
+ assert ip
+ assert ip.src == "192.0.2.1"
+ assert ip.dst == "192.0.2.2"
+ assert icmp
+ assert icmp.type == 3
+ assert icmp.code == 3
+ assert udp
+ assert udp.sport == 4321
+ assert udp.dport == 1234
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_sctp(self):
+ self.common_setup()
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ # Send an SCTP init, expect an SCTP abort
+ pkt = sp.IP(dst="198.51.100.2") \
+ / sp.SCTP(sport=1111, dport=2222) \
+ / sp.SCTPChunkInit(init_tag=1, n_in_streams=1, n_out_streams=1, a_rwnd=1500)
+ print(pkt)
+ reply = sp.sr1(pkt, timeout=3, verbose=False)
+ print(reply)
+ ip = reply.getlayer(sp.IP)
+ sctp = reply.getlayer(sp.SCTP)
+ abort = reply.getlayer(sp.SCTPChunkAbort)
+ print(sctp)
+
+ assert ip
+ assert ip.src == "198.51.100.2"
+ assert ip.dst == "192.0.2.2"
+ assert sctp
+ assert sctp.sport == 2222
+ assert sctp.dport == 1111
+ assert(abort)
diff --git a/tests/sys/netpfil/pf/ridentifier.sh b/tests/sys/netpfil/pf/ridentifier.sh
index c456d2111e20..8d83bcfb8213 100644
--- a/tests/sys/netpfil/pf/ridentifier.sh
+++ b/tests/sys/netpfil/pf/ridentifier.sh
@@ -45,7 +45,7 @@ basic_body()
vnet_mkjail alcatraz ${epair}b
jexec alcatraz ifconfig lo0 up
jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid $(atf_get_srcdir)/echo_inetd.conf
# Sanity check
atf_check -s exit:0 -o ignore ping -c 1 192.0.2.2
@@ -56,7 +56,7 @@ basic_body()
"pass in log" \
"pass in log proto tcp ridentifier 1234"
- jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log &
+ jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log &
sleep 1
echo "test" | nc -N 192.0.2.2 7
@@ -67,17 +67,17 @@ basic_body()
# Make sure we spotted the ridentifier
atf_check -s exit:0 -o ignore \
- grep 'rule 1/0.*ridentifier 1234' tcpdump.log
+ grep 'rule 1/0.*ridentifier 1234' ${PWD}/tcpdump.log
# But not on the !TCP traffic
atf_check -s exit:1 -o ignore \
- grep 'rule 0/0.*ridentifier' tcpdump.log
+ grep 'rule 0/0.*ridentifier' ${PWD}/tcpdump.log
# Now try with antispoof rules
pft_set_rules alcatraz \
"pass in log" \
"antispoof log for ${epair}b ridentifier 4321"
- jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > tcpdump.log &
+ jexec alcatraz tcpdump --immediate-mode -n -e -i pflog0 > ${PWD}/tcpdump.log &
sleep 1
# Without explicit rules for lo0 we're going to drop packets to ourself
@@ -87,18 +87,16 @@ basic_body()
sleep 1
jexec alcatraz killall tcpdump
- cat tcpdump.log
+ cat ${PWD}/tcpdump.log
# Make sure we spotted the ridentifier
atf_check -s exit:0 -o ignore \
- grep 'rule 2/0.*ridentifier 4321' tcpdump.log
+ grep 'rule 2/0.*ridentifier 4321' ${PWD}/tcpdump.log
}
basic_cleanup()
{
pft_cleanup
- rm -f inetd-alcatraz.pid
- rm -f tcpdump.log
}
atf_init_test_cases()
diff --git a/tests/sys/netpfil/pf/route_to.sh b/tests/sys/netpfil/pf/route_to.sh
index df95eaecc12e..5c0d355b8ea1 100644
--- a/tests/sys/netpfil/pf/route_to.sh
+++ b/tests/sys/netpfil/pf/route_to.sh
@@ -140,7 +140,7 @@ multiwan_body()
jexec srv sysctl net.inet.ip.forwarding=1
# Run echo server in srv jail
- jexec srv /usr/sbin/inetd -p multiwan.pid $(atf_get_srcdir)/echo_inetd.conf
+ jexec srv /usr/sbin/inetd -p ${PWD}/multiwan.pid $(atf_get_srcdir)/echo_inetd.conf
jexec srv pfctl -e
pft_set_rules srv \
@@ -178,7 +178,6 @@ multiwan_body()
multiwan_cleanup()
{
- rm -f multiwan.pid
pft_cleanup
}
@@ -257,7 +256,7 @@ icmp_nat_head()
{
atf_set descr 'Test that ICMP packets are correct for route-to + NAT'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
icmp_nat_body()
@@ -517,6 +516,7 @@ ifbound_reply_to_head()
{
atf_set descr 'Test that reply-to states bind to the expected interface'
atf_set require.user root
+ atf_set require.progs python3 scapy
}
ifbound_reply_to_body()
@@ -572,6 +572,7 @@ ifbound_reply_to_v6_head()
{
atf_set descr 'Test that reply-to states bind to the expected interface for IPv6'
atf_set require.user root
+ atf_set require.progs python3 scapy
}
ifbound_reply_to_v6_body()
@@ -631,6 +632,7 @@ ifbound_reply_to_rdr_dummynet_head()
{
atf_set descr 'Test that reply-to states bind to the expected non-default-route interface after rdr and dummynet'
atf_set require.user root
+ atf_set require.progs python3 scapy
}
ifbound_reply_to_rdr_dummynet_body()
@@ -787,6 +789,76 @@ dummynet_double_cleanup()
pft_cleanup
}
+atf_test_case "sticky" "cleanup"
+sticky_head()
+{
+ atf_set descr 'Set and retrieve a rule with sticky-address'
+ atf_set require.user root
+}
+
+sticky_body()
+{
+ pft_init
+
+ vnet_mkjail alcatraz
+
+ pft_set_rules alcatraz \
+ "pass in quick log on n_test_h_rtr route-to (n_srv_h_rtr <change_dst>) sticky-address from any to <dst> keep state"
+
+ jexec alcatraz pfctl -qvvsr
+}
+
+sticky_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "ttl" "cleanup"
+ttl_head()
+{
+ atf_set descr 'Ensure we decrement TTL on route-to'
+ atf_set require.user root
+}
+
+ttl_body()
+{
+ pft_init
+
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+ ifconfig ${epair_one}b 192.0.2.2/24 up
+ route add default 192.0.2.1
+
+ vnet_mkjail alcatraz ${epair_one}a ${epair_two}a
+ jexec alcatraz ifconfig ${epair_one}a 192.0.2.1/24 up
+ jexec alcatraz ifconfig ${epair_two}a 198.51.100.1/24 up
+ jexec alcatraz sysctl net.inet.ip.forwarding=1
+
+ vnet_mkjail singsing ${epair_two}b
+ jexec singsing ifconfig ${epair_two}b 198.51.100.2/24 up
+ jexec singsing route add default 198.51.100.1
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ ping -c 3 198.51.100.2
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "pass out" \
+ "pass in route-to (${epair_two}a 198.51.100.2)"
+
+ atf_check -s exit:0 -o ignore \
+ ping -c 3 198.51.100.2
+
+ atf_check -s exit:2 -o ignore \
+ ping -m 1 -c 3 198.51.100.2
+}
+
+ttl_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "v4"
@@ -803,4 +875,6 @@ atf_init_test_cases()
atf_add_test_case "ifbound_reply_to_rdr_dummynet"
atf_add_test_case "dummynet_frag"
atf_add_test_case "dummynet_double"
+ atf_add_test_case "sticky"
+ atf_add_test_case "ttl"
}
diff --git a/tests/sys/netpfil/pf/rtable.sh b/tests/sys/netpfil/pf/rtable.sh
index 62b37462e948..bb2cada57049 100644
--- a/tests/sys/netpfil/pf/rtable.sh
+++ b/tests/sys/netpfil/pf/rtable.sh
@@ -31,7 +31,7 @@ forward_v4_head()
{
atf_set descr 'Test IPv4 forwarding with rtable'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
forward_v4_body()
@@ -78,7 +78,7 @@ forward_v6_head()
{
atf_set descr 'Test IPv6 forwarding with rtable'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
forward_v6_body()
diff --git a/tests/sys/netpfil/pf/rules_counter.sh b/tests/sys/netpfil/pf/rules_counter.sh
index 962e7c93ba93..98f96a7adca1 100644
--- a/tests/sys/netpfil/pf/rules_counter.sh
+++ b/tests/sys/netpfil/pf/rules_counter.sh
@@ -148,6 +148,54 @@ keepcounters_body()
jexec alcatraz pfctl -s r -v
}
+atf_test_case "4G" "cleanup"
+4G_head()
+{
+ atf_set descr 'Test keepcounter for values above 32 bits'
+ atf_set require.user root
+}
+
+4G_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a 192.0.2.1/24 up
+
+ vnet_mkjail alcatraz ${epair}b
+ jexec alcatraz ifconfig ${epair}b 192.0.2.2/24 up
+ jexec alcatraz nc -l 1234 >/dev/null &
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "pass all"
+
+ # Now pass more than 4GB of data
+ dd if=/dev/zero bs=1k count=4M | nc -N 192.0.2.2 1234
+
+ bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }')
+ if [ $bytes -lt 4000000000 ];
+ then
+ atf_fail "Expected to see > 4GB"
+ fi
+
+ # Set new rules, keeping counters
+ pft_set_rules noflush alcatraz \
+ "set keepcounters" \
+ "pass all"
+
+ bytes=$(jexec alcatraz pfctl -s r -v | awk '/Bytes:/ { print $7; }')
+ if [ $bytes -lt 4000000000 ];
+ then
+ atf_fail "Expected to see > 4GB after rule reload"
+ fi
+}
+
+4G_cleanup()
+{
+ pft_cleanup
+}
+
keepcounters_cleanup()
{
pft_cleanup
@@ -157,4 +205,5 @@ atf_init_test_cases()
{
atf_add_test_case "get_clear"
atf_add_test_case "keepcounters"
+ atf_add_test_case "4G"
}
diff --git a/tests/sys/netpfil/pf/scrub.sh b/tests/sys/netpfil/pf/scrub.sh
index b9efdaf5205c..6a5b748bed7b 100644
--- a/tests/sys/netpfil/pf/scrub.sh
+++ b/tests/sys/netpfil/pf/scrub.sh
@@ -32,7 +32,7 @@ max_mss_v4_head()
{
atf_set descr 'Test IPv4 scrub "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v4_body()
@@ -57,7 +57,7 @@ max_mss_v6_head()
{
atf_set descr 'Test IPv6 scrub "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v6_body()
@@ -82,7 +82,7 @@ set_tos_v4_head()
{
atf_set descr 'Test IPv4 scub "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v4_body()
@@ -103,7 +103,7 @@ set_tos_v6_head()
{
atf_set descr 'Test IPv6 scub "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v6_body()
@@ -124,7 +124,7 @@ min_ttl_v4_head()
{
atf_set descr 'Test IPv4 scub "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v4_body()
@@ -145,7 +145,7 @@ min_ttl_v6_head()
{
atf_set descr 'Test IPv6 scub "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v6_body()
@@ -166,7 +166,7 @@ no_scrub_v4_head()
{
atf_set descr 'Test IPv4 "no scrub" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
no_scrub_v4_body()
@@ -189,7 +189,7 @@ no_scrub_v6_head()
{
atf_set descr 'Test IPv6 "no scrub" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
no_scrub_v6_body()
diff --git a/tests/sys/netpfil/pf/scrub_compat.sh b/tests/sys/netpfil/pf/scrub_compat.sh
index cf69da3f3b74..6e1499309869 100644
--- a/tests/sys/netpfil/pf/scrub_compat.sh
+++ b/tests/sys/netpfil/pf/scrub_compat.sh
@@ -33,7 +33,7 @@ max_mss_v4_head()
{
atf_set descr 'Test IPv4 scrub "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v4_body()
@@ -58,7 +58,7 @@ max_mss_v6_head()
{
atf_set descr 'Test IPv6 scrub "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v6_body()
@@ -83,7 +83,7 @@ set_tos_v4_head()
{
atf_set descr 'Test IPv4 scub "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v4_body()
@@ -104,7 +104,7 @@ set_tos_v6_head()
{
atf_set descr 'Test IPv6 scub "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v6_body()
@@ -125,7 +125,7 @@ min_ttl_v4_head()
{
atf_set descr 'Test IPv4 scub "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v4_body()
@@ -146,7 +146,7 @@ min_ttl_v6_head()
{
atf_set descr 'Test IPv6 scub "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v6_body()
@@ -167,7 +167,7 @@ no_scrub_v4_head()
{
atf_set descr 'Test IPv4 "no scrub" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
no_scrub_v4_body()
@@ -190,7 +190,7 @@ no_scrub_v6_head()
{
atf_set descr 'Test IPv6 "no scrub" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
no_scrub_v6_body()
diff --git a/tests/sys/netpfil/pf/scrub_pass.sh b/tests/sys/netpfil/pf/scrub_pass.sh
index 319d805144a0..8ba599144757 100644
--- a/tests/sys/netpfil/pf/scrub_pass.sh
+++ b/tests/sys/netpfil/pf/scrub_pass.sh
@@ -33,7 +33,7 @@ max_mss_v4_head()
{
atf_set descr 'Test IPv4 pass "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v4_body()
@@ -58,7 +58,7 @@ max_mss_v6_head()
{
atf_set descr 'Test IPv6 pass "mss" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
max_mss_v6_body()
@@ -83,7 +83,7 @@ set_tos_v4_head()
{
atf_set descr 'Test IPv4 pass "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v4_body()
@@ -104,7 +104,7 @@ set_tos_v6_head()
{
atf_set descr 'Test IPv6 pass "set-tos" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
set_tos_v6_body()
@@ -125,7 +125,7 @@ min_ttl_v4_head()
{
atf_set descr 'Test IPv4 pass "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v4_body()
@@ -146,7 +146,7 @@ min_ttl_v6_head()
{
atf_set descr 'Test IPv6 pass "min-ttl" rule'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
min_ttl_v6_body()
diff --git a/tests/sys/netpfil/pf/sctp.py b/tests/sys/netpfil/pf/sctp.py
index 6042badffb64..da42ce527195 100644
--- a/tests/sys/netpfil/pf/sctp.py
+++ b/tests/sys/netpfil/pf/sctp.py
@@ -268,7 +268,8 @@ class TestSCTP(VnetTestTemplate):
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
"block proto sctp",
- "pass inet proto sctp to 192.0.2.0/24"])
+ "pass inet proto sctp to 192.0.2.0/24",
+ "pass on lo"])
# Sanity check, we can communicate with the primary address.
client = SCTPClient("192.0.2.3", 1234)
@@ -305,6 +306,7 @@ class TestSCTP(VnetTestTemplate):
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
"block proto sctp",
+ "pass on lo",
"pass inet proto sctp from 192.0.2.0/24"])
# Sanity check, we can communicate with the primary address.
@@ -362,7 +364,7 @@ class TestSCTP(VnetTestTemplate):
@pytest.mark.require_user("root")
- def test_permutation(self):
+ def test_permutation_if_bound(self):
# Test that we generate all permutations of src/dst addresses.
# Assign two addresses to each end, and check for the expected states
srv_vnet = self.vnet_map["vnet2"]
@@ -374,6 +376,7 @@ class TestSCTP(VnetTestTemplate):
ToolsHelper.pf_rules([
"set state-policy if-bound",
"block proto sctp",
+ "pass on lo",
"pass inet proto sctp to 192.0.2.0/24"])
# Sanity check, we can communicate with the primary address.
@@ -387,11 +390,146 @@ class TestSCTP(VnetTestTemplate):
# Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4
states = ToolsHelper.get_output("/sbin/pfctl -ss")
print(states)
- assert re.search(r".*sctp 192.0.2.1:.*192.0.2.3:1234", states)
+ assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states)
+ assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states)
+ assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.3:1234", states)
+ assert re.search(r"epair.*sctp 192.0.2.4:.*192.0.2.2:1234", states)
+
+ @pytest.mark.require_user("root")
+ def test_permutation_floating(self):
+ # Test that we generate all permutations of src/dst addresses.
+ # Assign two addresses to each end, and check for the expected states
+ srv_vnet = self.vnet_map["vnet2"]
+
+ ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name
+ ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.4/24" % ifname)
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "block proto sctp",
+ "pass on lo",
+ "pass inet proto sctp to 192.0.2.0/24"])
+
+ # Sanity check, we can communicate with the primary address.
+ client = SCTPClient("192.0.2.3", 1234)
+ client.send(b"hello", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "hello"
+
+ # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1, but also to 192.0.2.4
+ states = ToolsHelper.get_output("/sbin/pfctl -ss")
+ print(states)
+ assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states)
assert re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states)
- assert re.search(r".*sctp 192.0.2.4:.*192.0.2.3:1234", states)
+ assert re.search(r"all sctp 192.0.2.4:.*192.0.2.3:1234", states)
assert re.search(r"all sctp 192.0.2.4:.*192.0.2.2:1234", states)
+ @pytest.mark.require_user("root")
+ def test_limit_addresses(self):
+ srv_vnet = self.vnet_map["vnet2"]
+
+ ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name
+ for i in range(0, 16):
+ ToolsHelper.print_output("/sbin/ifconfig %s inet alias 192.0.2.%d/24" % (ifname, 4 + i))
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "block proto sctp",
+ "pass on lo",
+ "pass inet proto sctp to 192.0.2.0/24"])
+
+ # Set up a connection, which will try to create states for all addresses
+ # we have assigned
+ client = SCTPClient("192.0.2.3", 1234)
+ client.send(b"hello", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "hello"
+
+ # But the number should be limited to 9 (original + 8 extra)
+ states = ToolsHelper.get_output("/sbin/pfctl -ss | grep 192.0.2.2")
+ print(states)
+ assert(states.count('\n') <= 9)
+
+ @pytest.mark.require_user("root")
+ def test_disallow_related(self):
+ srv_vnet = self.vnet_map["vnet2"]
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "block proto sctp",
+ "pass inet proto sctp to 192.0.2.3",
+ "pass on lo"])
+
+ # Sanity check, we can communicate with the primary address.
+ client = SCTPClient("192.0.2.3", 1234)
+ client.send(b"hello", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "hello"
+
+ # This shouldn't work
+ success=False
+ try:
+ client.newpeer("192.0.2.2")
+ client.send(b"world", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "world"
+ success=True
+ except:
+ success=False
+ assert not success
+
+ # Check that we have a state for 192.0.2.3, but not 192.0.2.2 to 192.0.2.1
+ states = ToolsHelper.get_output("/sbin/pfctl -ss")
+ assert re.search(r"all sctp 192.0.2.1:.*192.0.2.3:1234", states)
+ assert not re.search(r"all sctp 192.0.2.1:.*192.0.2.2:1234", states)
+
+ @pytest.mark.require_user("root")
+ def test_allow_related(self):
+ srv_vnet = self.vnet_map["vnet2"]
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "set state-policy if-bound",
+ "block proto sctp",
+ "pass inet proto sctp to 192.0.2.3 keep state (allow-related)",
+ "pass on lo"])
+
+ # Sanity check, we can communicate with the primary address.
+ client = SCTPClient("192.0.2.3", 1234)
+ client.send(b"hello", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "hello"
+
+ success=False
+ try:
+ client.newpeer("192.0.2.2")
+ client.send(b"world", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "world"
+ success=True
+ finally:
+ # Debug output
+ ToolsHelper.print_output("/sbin/pfctl -ss")
+ ToolsHelper.print_output("/sbin/pfctl -sr -vv")
+ assert success
+
+ # Check that we have a state for 192.0.2.3 and 192.0.2.2 to 192.0.2.1
+ states = ToolsHelper.get_output("/sbin/pfctl -ss")
+ assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.3:1234", states)
+ assert re.search(r"epair.*sctp 192.0.2.1:.*192.0.2.2:1234", states)
+
class TestSCTPv6(VnetTestTemplate):
REQUIRED_MODULES = ["sctp", "pf"]
TOPOLOGY = {
@@ -417,6 +555,7 @@ class TestSCTPv6(VnetTestTemplate):
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
"block proto sctp",
+ "pass on lo",
"pass inet6 proto sctp to 2001:db8::0/64"])
# Sanity check, we can communicate with the primary address.
@@ -454,6 +593,7 @@ class TestSCTPv6(VnetTestTemplate):
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
"block proto sctp",
+ "pass on lo",
"pass inet6 proto sctp from 2001:db8::/64"])
# Sanity check, we can communicate with the primary address.
@@ -520,7 +660,40 @@ class TestSCTPv6(VnetTestTemplate):
ToolsHelper.print_output("/sbin/pfctl -e")
ToolsHelper.pf_rules([
+ "set state-policy if-bound",
+ "block proto sctp",
+ "pass on lo",
+ "pass inet6 proto sctp to 2001:db8::0/64"])
+
+ # Sanity check, we can communicate with the primary address.
+ client = SCTPClient("2001:db8::3", 1234)
+ client.send(b"hello", 0)
+ rcvd = self.wait_object(srv_vnet.pipe)
+ print(rcvd)
+ assert rcvd['ppid'] == 0
+ assert rcvd['data'] == "hello"
+
+ # Check that we have a state for 2001:db8::3 and 2001:db8::2 to 2001:db8::1, but also to 2001:db8::4
+ states = ToolsHelper.get_output("/sbin/pfctl -ss")
+ print(states)
+ assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::2\[1234\]", states)
+ assert re.search(r"epair.*sctp 2001:db8::1\[.*2001:db8::3\[1234\]", states)
+ assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::2\[1234\]", states)
+ assert re.search(r"epair.*sctp 2001:db8::4\[.*2001:db8::3\[1234\]", states)
+
+ @pytest.mark.require_user("root")
+ def test_permutation_floating(self):
+ # Test that we generate all permutations of src/dst addresses.
+ # Assign two addresses to each end, and check for the expected states
+ srv_vnet = self.vnet_map["vnet2"]
+
+ ifname = self.vnet_map["vnet1"].iface_alias_map["if1"].name
+ ToolsHelper.print_output("/sbin/ifconfig %s inet6 alias 2001:db8::4/64" % ifname)
+
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
"block proto sctp",
+ "pass on lo",
"pass inet6 proto sctp to 2001:db8::0/64"])
# Sanity check, we can communicate with the primary address.
diff --git a/tests/sys/netpfil/pf/sctp.sh b/tests/sys/netpfil/pf/sctp.sh
index 66225e132d1c..57dcdad1d866 100644
--- a/tests/sys/netpfil/pf/sctp.sh
+++ b/tests/sys/netpfil/pf/sctp.sh
@@ -181,6 +181,64 @@ basic_v6_cleanup()
pft_cleanup
}
+atf_test_case "reuse" "cleanup"
+reuse_head()
+{
+ atf_set descr 'Test handling dumb clients that reuse source ports'
+ atf_set require.user root
+}
+
+reuse_body()
+{
+ sctp_init
+
+ j="sctp:reuse"
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail ${j}a ${epair}a
+ vnet_mkjail ${j}b ${epair}b
+
+ jexec ${j}a ifconfig ${epair}a 192.0.2.1/24 up
+ jexec ${j}b ifconfig ${epair}b 192.0.2.2/24 up
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ jexec ${j}a ping -c 1 192.0.2.2
+
+ jexec ${j}a pfctl -e
+ pft_set_rules ${j}a \
+ "block" \
+ "pass in proto sctp to port 1234"
+
+ echo "foo" | jexec ${j}a nc --sctp -N -l 1234 &
+
+ # Wait for the server to start
+ sleep 1
+
+ out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234)
+ if [ "$out" != "foo" ]; then
+ atf_fail "SCTP connection failed"
+ fi
+
+ # Now do the same thing again, with the same port numbers
+ jexec ${j}a pfctl -ss -v
+
+ echo "foo" | jexec ${j}a nc --sctp -N -l 1234 &
+
+ # Wait for the server to start
+ sleep 1
+
+ out=$(jexec ${j}b nc --sctp -N -w 3 -p 1234 192.0.2.1 1234)
+ if [ "$out" != "foo" ]; then
+ atf_fail "SCTP connection failed"
+ fi
+ jexec ${j}a pfctl -ss -v
+}
+
+reuse_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "abort_v4" "cleanup"
abort_v4_head()
{
@@ -504,6 +562,7 @@ pfsync_body()
sctp_init
pfsynct_init
+ vnet_init_bridge
if ! kldstat -q -m carp
then
atf_skip "This test requires carp"
@@ -655,14 +714,133 @@ pfsync_cleanup()
pfsynct_cleanup
}
+atf_test_case "timeout" "cleanup"
+timeout_head()
+{
+ atf_set descr 'Test setting and retrieving timeout values'
+ atf_set require.user root
+}
+
+timeout_body()
+{
+ sctp_init
+
+ vnet_mkjail timeout
+
+ pft_set_rules timeout \
+ "set timeout sctp.first 13" \
+ "set timeout sctp.opening 14"
+
+ atf_check -s exit:0 -o match:"sctp.first.*13" \
+ jexec timeout pfctl -st
+ atf_check -s exit:0 -o match:"sctp.opening.*14" \
+ jexec timeout pfctl -st
+ # We've not changed other timeouts
+ atf_check -s exit:0 -o match:"sctp.established.*86400" \
+ jexec timeout pfctl -st
+}
+
+timeout_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "related_icmp" "cleanup"
+related_icmp_head()
+{
+ atf_set descr 'Verify that ICMP messages related to an SCTP connection are allowed'
+ atf_set require.user root
+}
+
+related_icmp_body()
+{
+ sctp_init
+
+ epair_cl=$(vnet_mkepair)
+ epair_rtr=$(vnet_mkepair)
+ epair_srv=$(vnet_mkepair)
+
+ ifconfig ${epair_cl}a 192.0.2.1/24 up
+ route add default 192.0.2.2
+
+ vnet_mkjail rtr ${epair_cl}b ${epair_rtr}a
+ jexec rtr ifconfig ${epair_cl}b 192.0.2.2/24 up
+ jexec rtr ifconfig ${epair_rtr}a 198.51.100.1/24 up
+ jexec rtr sysctl net.inet.ip.forwarding=1
+ jexec rtr route add default 198.51.100.2
+
+ vnet_mkjail rtr2 ${epair_rtr}b ${epair_srv}a
+ jexec rtr2 ifconfig ${epair_rtr}b 198.51.100.2/24 up
+ jexec rtr2 ifconfig ${epair_srv}a 203.0.113.1/24 up
+ jexec rtr2 ifconfig ${epair_srv}a mtu 1300
+ jexec rtr2 sysctl net.inet.ip.forwarding=1
+ jexec rtr2 route add default 198.51.100.1
+
+ vnet_mkjail srv ${epair_srv}b
+ jexec srv ifconfig ${epair_srv}b 203.0.113.2/24 up
+ jexec srv ifconfig ${epair_srv}b mtu 1300
+ jexec srv route add default 203.0.113.1
+
+ # Sanity checks
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.2
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 198.51.100.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 198.51.100.2
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 203.0.113.2
+
+ jexec rtr pfctl -e
+ pft_set_rules rtr \
+ "block proto icmp" \
+ "pass proto sctp"
+
+ # Make sure SCTP traffic passes
+ echo "foo" | jexec srv nc --sctp -N -l 1234 &
+ sleep 1
+
+ out=$(nc --sctp -N -w 3 203.0.113.2 1234)
+ if [ "$out" != "foo" ]; then
+ jexec rtr pfctl -ss -vv
+ jexec rtr pfctl -sr -vv
+ atf_fail "SCTP connection failed"
+ fi
+
+ # Do we see ICMP traffic if we send overly large traffic?
+ echo "foo" | jexec srv nc --sctp -l 1234 >/dev/null &
+ sleep 1
+
+ atf_check -s exit:0 -o not-match:".*destination unreachable:.*" \
+ netstat -s -p icmp
+
+ # Generate traffic that will be fragmented by rtr2, and will provoke an
+ # ICMP unreachable - need to frag (mtu 1300) message
+ dd if=/dev/random bs=10000 count=1 | nc --sctp -N -w 3 203.0.113.2 1234
+
+ # We'd expect to see an ICMP message
+ atf_check -s exit:0 -o match:".*destination unreachable: [1-9]" \
+ netstat -s -p icmp
+}
+
+related_icmp_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "basic_v4"
atf_add_test_case "basic_v6"
+ atf_add_test_case "reuse"
atf_add_test_case "abort_v4"
atf_add_test_case "abort_v6"
atf_add_test_case "nat_v4"
atf_add_test_case "nat_v6"
atf_add_test_case "rdr_v4"
atf_add_test_case "pfsync"
+ atf_add_test_case "timeout"
+ atf_add_test_case "related_icmp"
}
diff --git a/tests/sys/netpfil/pf/set_skip.sh b/tests/sys/netpfil/pf/set_skip.sh
index e5b1440360e9..e984377721b8 100644
--- a/tests/sys/netpfil/pf/set_skip.sh
+++ b/tests/sys/netpfil/pf/set_skip.sh
@@ -26,6 +26,50 @@
. $(atf_get_srcdir)/utils.subr
+atf_test_case "unset" "cleanup"
+unset_head()
+{
+ atf_set descr 'Unset set skip test'
+ atf_set require.user root
+}
+
+unset_body()
+{
+ pft_init
+
+ vnet_mkjail alcatraz
+ jexec alcatraz ifconfig lo0 127.0.0.1/8 up
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz "set skip on lo0" \
+ "block in proto icmp"
+
+ echo "set skip"
+ jexec alcatraz pfctl -v -sI
+
+ jexec alcatraz ifconfig
+ atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1
+
+ # Unset the skip on the group
+ pft_set_rules noflush alcatraz \
+ "block in proto icmp"
+
+ echo "No setskip"
+ jexec alcatraz pfctl -v -sI
+
+ # Do flush states
+ jexec alcatraz pfctl -Fs
+
+ # And now our ping is blocked
+ atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1
+
+ jexec alcatraz pfctl -v -sI
+}
+
+unset_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "set_skip_group" "cleanup"
set_skip_group_head()
{
@@ -45,8 +89,24 @@ set_skip_group_body()
pft_set_rules alcatraz "set skip on foo" \
"block in proto icmp"
+ echo "set skip"
+ jexec alcatraz pfctl -v -sI
+
jexec alcatraz ifconfig
atf_check -s exit:0 -o ignore jexec alcatraz ping -c 1 127.0.0.1
+
+ # Unset the skip on the group
+ pft_set_rules noflush alcatraz \
+ "block in proto icmp"
+
+ # Do flush states
+ jexec alcatraz pfctl -Fs
+
+ # And now our ping is blocked
+ atf_check -s exit:2 -o ignore jexec alcatraz ping -c 1 127.0.0.1
+
+ echo "No setskip"
+ jexec alcatraz pfctl -v -sI
}
set_skip_group_cleanup()
@@ -163,6 +223,7 @@ pr255852_cleanup()
atf_init_test_cases()
{
+ atf_add_test_case "unset"
atf_add_test_case "set_skip_group"
atf_add_test_case "set_skip_group_lo"
atf_add_test_case "set_skip_dynamic"
diff --git a/tests/sys/netpfil/pf/set_tos.sh b/tests/sys/netpfil/pf/set_tos.sh
index bfec61f0d221..75b96edbab6e 100644
--- a/tests/sys/netpfil/pf/set_tos.sh
+++ b/tests/sys/netpfil/pf/set_tos.sh
@@ -37,7 +37,7 @@ v4_head()
atf_set require.user root
# We need scapy to be installed for out test scripts to work
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v4_body()
@@ -122,7 +122,7 @@ v6_head()
atf_set require.user root
# We need scapy to be installed for out test scripts to work
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
v6_body()
@@ -191,6 +191,22 @@ v6_body()
--to 2001:db8:192::2 \
--replyif ${epair}a \
--expect-tc 0
+
+ # We can set tos on pass rules
+ pft_set_rules alcatraz "pass out set tos 13"
+ atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \
+ --sendif ${epair}a \
+ --to 2001:db8:192::2 \
+ --replyif ${epair}a \
+ --expect-tc 13
+
+ # And that still works with 'scrub' options too
+ pft_set_rules alcatraz "pass out set tos 14 scrub (min-ttl 64)"
+ atf_check -s exit:0 -o ignore -e ignore ${common_dir}/pft_ping.py \
+ --sendif ${epair}a \
+ --to 2001:db8:192::2 \
+ --replyif ${epair}a \
+ --expect-tc 14
}
v6_cleanup()
diff --git a/tests/sys/netpfil/pf/snmp.sh b/tests/sys/netpfil/pf/snmp.sh
new file mode 100644
index 000000000000..37cc4b75cf92
--- /dev/null
+++ b/tests/sys/netpfil/pf/snmp.sh
@@ -0,0 +1,123 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Kristof Provost <kp@FreeBSD.org>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Basic pf_snmp test'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ # Start bsnmpd
+ jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "pass"
+
+ # Sanity check, and create state
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # pf should be enabled
+ atf_check -s exit:0 -o match:'pfStatusRunning.0 = true' \
+ bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "table" "cleanup"
+table_head()
+{
+ atf_set descr 'Test tables and pf_snmp'
+ atf_set require.user root
+}
+
+table_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}b 192.0.2.2/24 up
+
+ vnet_mkjail alcatraz ${epair}a
+ jexec alcatraz ifconfig ${epair}a 192.0.2.1/24 up
+
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "table <foo> counters { 192.0.2.0/24 }" \
+ "pass in from <foo>"
+
+ # Start bsnmpd after creating the table so we don't have to wait for
+ # a refresh timeout
+ jexec alcatraz bsnmpd -c $(atf_get_srcdir)/bsnmpd.conf
+
+ # Sanity check, and create state
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 192.0.2.1
+
+ # We should have one table
+ atf_check -s exit:0 -o match:'pfTablesTblNumber.0 = 1' \
+ bsnmpwalk -s public@192.0.2.1 -i pf_tree.def begemot
+
+ # We have the 'foo' table
+ atf_check -s exit:0 -o match:'pfTablesTblDescr.* = foo' \
+ bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables
+
+ # Which contains address 192.0.2.0/24
+ atf_check -s exit:0 -o match:'pfTablesAddrNet.* = 192.0.2.0' \
+ bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables
+ atf_check -s exit:0 -o match:'pfTablesAddrPrefix.* = 24' \
+ bsnmpwalk -s public@192.0.2.1 -i pf_tree.def pfTables
+}
+
+table_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+ atf_add_test_case "table"
+}
diff --git a/tests/sys/netpfil/pf/src_track.sh b/tests/sys/netpfil/pf/src_track.sh
index 27eb62abcf41..c24f88062c4d 100755
--- a/tests/sys/netpfil/pf/src_track.sh
+++ b/tests/sys/netpfil/pf/src_track.sh
@@ -2,6 +2,7 @@
# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2020 Kristof Provost <kp@FreeBSD.org>
+# Copyright (c) 2024 Kajetan Staszkiewicz <vegeta@tuxpowered.net>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
@@ -51,7 +52,19 @@ source_track_body()
"pass out keep state (source-track)"
ping -c 3 192.0.2.1
- jexec alcatraz pfctl -s all -v
+ atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+
+ # Flush all source nodes
+ jexec alcatraz pfctl -FS
+
+ # We can't find the previous source node any more
+ atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+
+ # But we still have the state
+ atf_check -s exit:0 -o match:'all icmp 192.0.2.1:8 <- 192.0.2.2:.*' \
+ jexec alcatraz pfctl -ss
}
source_track_cleanup()
@@ -59,7 +72,444 @@ source_track_cleanup()
pft_cleanup
}
+atf_test_case "kill" "cleanup"
+kill_head()
+{
+ atf_set descr 'Test killing source nodes'
+ atf_set require.user root
+}
+
+kill_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail alcatraz ${epair}b
+
+ ifconfig ${epair}a 192.0.2.2/24 up
+ ifconfig ${epair}a inet alias 192.0.2.3/24 up
+ jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up
+
+ # Enable pf!
+ jexec alcatraz pfctl -e
+ pft_set_rules alcatraz \
+ "pass in keep state (source-track)" \
+ "pass out keep state (source-track)"
+
+ # Establish two sources
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 -S 192.0.2.2 192.0.2.1
+ atf_check -s exit:0 -o ignore \
+ ping -c 1 -S 192.0.2.3 192.0.2.1
+
+ # Check that both source nodes exist
+ atf_check -s exit:0 -o match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+ atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+
+
+jexec alcatraz pfctl -sS
+
+ # Kill the 192.0.2.2 source
+ jexec alcatraz pfctl -K 192.0.2.2
+
+ # The other source still exists
+ atf_check -s exit:0 -o match:'192.0.2.3 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+
+ # But not the one we killed
+ atf_check -s exit:0 -o not-match:'192.0.2.2 -> 0.0.0.0 \( states 1,.*' \
+ jexec alcatraz pfctl -sS
+}
+
+kill_cleanup()
+{
+ pft_cleanup
+}
+
+max_src_conn_rule_head()
+{
+ atf_set descr 'Max connections per source per rule'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+max_src_conn_rule_body()
+{
+ setup_router_server_ipv6
+
+ # Clients will connect from another network behind the router.
+ # This allows for using multiple source addresses and for tester jail
+ # to not respond with RST packets for SYN+ACKs.
+ jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2
+ jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1
+
+ pft_set_rules router \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in on ${epair_tester}b inet6 proto tcp keep state (max-src-conn 3 source-track rule overload <bad_hosts>)" \
+ "pass out on ${epair_server}a inet6 proto tcp keep state"
+
+ # Limiting of connections is done for connections which have successfully
+ # finished the 3-way handshake. Once the handshake is done, the state
+ # is moved to CLOSED state. We use pft_ping.py to check that the handshake
+ # was really successful and after that we check what is in pf state table.
+
+ # 3 connections from host ::1 will be allowed.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4201 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4202 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4203 --fromaddr 2001:db8:44::1
+ # The 4th connection from host ::1 will have its state killed.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4204 --fromaddr 2001:db8:44::1
+ # A connection from host :2 is will be allowed.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4205 --fromaddr 2001:db8:44::2
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qss | normalize_pfctl_s | grep 'tcp 2001:db8:43::2\[9\] <-' > $states
+
+ grep -qE '2001:db8:44::1\[4201\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4201 not found or not established"
+ grep -qE '2001:db8:44::1\[4202\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4202 not found or not established"
+ grep -qE '2001:db8:44::1\[4203\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4203 not found or not established"
+ grep -qE '2001:db8:44::2\[4205\] ESTABLISHED:ESTABLISHED' $states || atf_fail "State for port 4205 not found or not established"
+
+ if (
+ grep -qE '2001:db8:44::1\[4204\] ' $states &&
+ ! grep -qE '2001:db8:44::1\[4204\] CLOSED:CLOSED' $states
+ ); then
+ atf_fail "State for port 4204 found but not closed"
+ fi
+
+ jexec router pfctl -T test -t bad_hosts 2001:db8:44::1 || atf_fail "Host not found in overload table"
+}
+
+max_src_conn_rule_cleanup()
+{
+ pft_cleanup
+}
+
+max_src_states_rule_head()
+{
+ atf_set descr 'Max states per source per rule'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+max_src_states_rule_body()
+{
+ setup_router_server_ipv6
+
+ # Clients will connect from another network behind the router.
+ # This allows for using multiple source addresses and for tester jail
+ # to not respond with RST packets for SYN+ACKs.
+ jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2
+ jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1
+
+ pft_set_rules router \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track rule) label rule_A" \
+ "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track rule) label rule_B" \
+ "pass out on ${epair_server}a keep state"
+
+ # The option max-src-states prevents even the initial SYN packet going
+ # through. It's enough that we check ping_server_check_reply, no need to
+ # bother checking created states.
+
+ # 2 connections from host ::1 matching rule_A will be allowed, 1 will fail to create a state.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1
+
+ # 2 connections from host ::1 matching rule_B will be allowed, 1 will fail to create a state.
+ # Limits from rule_A don't interfere with rule_B.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4222 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4223 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::1
+
+ # 2 connections from host ::2 matching rule_B will be allowed, 1 will fail to create a state.
+ # Limits for host ::1 will not interfere with host ::2.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4224 --fromaddr 2001:db8:44::2
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4225 --fromaddr 2001:db8:44::2
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4226 --fromaddr 2001:db8:44::2
+ ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4227 --fromaddr 2001:db8:44::2
+
+ # We will check the resulting source nodes, though.
+ # Order of source nodes in output is not guaranteed, find each one separately.
+ nodes=$(mktemp) || exit 1
+ jexec router pfctl -qvsS | normalize_pfctl_s > $nodes
+ for node_regexp in \
+ '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 3, limit source-track$' \
+ '2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \
+ '2001:db8:44::2 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, filter rule 4, limit source-track$' \
+ ; do
+ grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'"
+ done
+
+ # Check if limit counters have been properly set.
+ jexec router pfctl -qvvsi | grep -qE 'max-src-states\s+3\s+' || atf_fail "max-src-states not set to 3"
+}
+
+max_src_states_rule_cleanup()
+{
+ pft_cleanup
+}
+
+max_src_states_global_head()
+{
+ atf_set descr 'Max states per source global'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+max_src_states_global_body()
+{
+ setup_router_server_ipv6
+
+ # Clients will connect from another network behind the router.
+ # This allows for using multiple source addresses and for tester jail
+ # to not respond with RST packets for SYN+ACKs.
+ jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2
+ jexec server route add -6 2001:db8:44::0/64 2001:db8:43::1
+
+ pft_set_rules router \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in on ${epair_tester}b inet6 proto tcp from port 4210:4219 keep state (max-src-states 3 source-track global) label rule_A" \
+ "pass in on ${epair_tester}b inet6 proto tcp from port 4220:4229 keep state (max-src-states 3 source-track global) label rule_B" \
+ "pass out on ${epair_server}a keep state"
+
+ # Global source tracking creates a single source node shared between all
+ # rules for each connecting source IP address and counts states created
+ # by all rules. Each rule has its own max-src-conn value checked against
+ # that single source node.
+
+ # 3 connections from host …::1 matching rule_A will be allowed.
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4211 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4212 --fromaddr 2001:db8:44::1
+ ping_server_check_reply exit:0 --ping-type=tcp3way --send-sport=4213 --fromaddr 2001:db8:44::1
+ # The 4th connection matching rule_A from host …::1 will have its state killed.
+ ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4214 --fromaddr 2001:db8:44::1
+ # A connection matching rule_B from host …::1 will have its state killed too.
+ ping_server_check_reply exit:1 --ping-type=tcp3way --send-sport=4221 --fromaddr 2001:db8:44::1
+
+ nodes=$(mktemp) || exit 1
+ jexec router pfctl -qvsS | normalize_pfctl_s > $nodes
+ cat $nodes
+ node_regexp='2001:db8:44::1 -> :: \( states 3, connections 3, rate [0-9/\.]+s \) age [0-9:]+, 9 pkts, [0-9]+ bytes, limit source-track'
+ grep -qE "$node_regexp" $nodes || atf_fail "Source nodes not matching expected output"
+}
+
+max_src_states_global_cleanup()
+{
+ pft_cleanup
+}
+
+sn_types_compat_head()
+{
+ atf_set descr 'Combination of source node types with compat NAT rules'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+sn_types_compat_body()
+{
+ setup_router_dummy_ipv6
+
+ # Clients will connect from another network behind the router.
+ # This allows for using multiple source addresses.
+ jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2
+
+ # Additional gateways for route-to.
+ rtgw=${net_server_host_server%::*}::2:1
+ jexec router ndp -s ${rtgw} 00:01:02:03:04:05
+
+ # This test will check for proper source node creation for:
+ # max-src-states -> PF_SN_LIMIT
+ # sticky-address -> PF_SN_NAT
+ # route-to -> PF_SN_ROUTE
+ # The test expands to all 8 combinations of those source nodes being
+ # present or not.
+
+ pft_set_rules router \
+ "table <rtgws> { ${rtgw} }" \
+ "table <rdrgws> { 2001:db8:45::1 }" \
+ "rdr on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 -> <rdrgws> port 4242 sticky-address" \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_3" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_4" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_5" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_6" \
+ "pass out quick on ${epair_server}a keep state"
+
+ # We don't check if state limits are properly enforced, this is tested
+ # by other tests in this file.
+ # Source address will not match the NAT rule
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1
+ # Source address will match the NAT rule
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+ nodes=$(mktemp) || exit 1
+ jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes
+
+ # Order of states in output is not guaranteed, find each one separately.
+ for state_regexp in \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, route sticky-address$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, route sticky-address$' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 3, NAT/RDR sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address, route sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 5, limit source-track, NAT/RDR sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address, route sticky-address' \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+
+ # Order of source nodes in output is not guaranteed, find each one separately.
+ for node_regexp in \
+ '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \
+ '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \
+ '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \
+ '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \
+ '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \
+ '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \
+ '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 4, route sticky-address' \
+ '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \
+ '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, limit source-track' \
+ '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, rdr rule 0, NAT/RDR sticky-address' \
+ '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, route sticky-address' \
+ '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \
+ ; do
+ grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'"
+ done
+
+ ! grep -q 'filter rule 3' $nodes || atf_fail "Source node found for rule 3"
+}
+
+sn_types_compat_cleanup()
+{
+ pft_cleanup
+}
+
+sn_types_pass_head()
+{
+ atf_set descr 'Combination of source node types with pass NAT rules'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+sn_types_pass_body()
+{
+ setup_router_dummy_ipv6
+
+ # Clients will connect from another network behind the router.
+ # This allows for using multiple source addresses.
+ jexec router route add -6 2001:db8:44::0/64 2001:db8:42::2
+
+ # Additional gateways for route-to.
+ rtgw=${net_server_host_server%::*}::2:1
+ jexec router ndp -s ${rtgw} 00:01:02:03:04:05
+
+ # This test will check for proper source node creation for:
+ # max-src-states -> PF_SN_LIMIT
+ # sticky-address -> PF_SN_NAT
+ # route-to -> PF_SN_ROUTE
+ # The test expands to all 8 combinations of those source nodes being
+ # present or not.
+
+ pft_set_rules router \
+ "table <rtgws> { ${rtgw} }" \
+ "table <rdrgws> { 2001:db8:45::1 }" \
+ "block" \
+ "pass inet6 proto icmp6 icmp6-type { neighbrsol, neighbradv }" \
+ "match in on ${epair_tester}b inet6 proto tcp from 2001:db8:44::10/124 to 2001:db8:45::1 rdr-to <rdrgws> port 4242 sticky-address label rule_3" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4211 keep state label rule_4" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4212 keep state label rule_5" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) inet6 proto tcp from port 4213 keep state (max-src-states 3 source-track rule) label rule_6" \
+ "pass in quick on ${epair_tester}b route-to ( ${epair_server}a <rtgws>) sticky-address inet6 proto tcp from port 4214 keep state (max-src-states 3 source-track rule) label rule_7" \
+ "pass out quick on ${epair_server}a keep state"
+
+ # We don't check if state limits are properly enforced, this is tested
+ # by other tests in this file.
+ # Source address will not match the NAT rule
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::01 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::02 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::03 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::04 --to 2001:db8:45::1
+ # Source address will match the NAT rule
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4211 --fromaddr 2001:db8:44::11 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4212 --fromaddr 2001:db8:44::12 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4213 --fromaddr 2001:db8:44::13 --to 2001:db8:45::1
+ ping_dummy_check_request exit:0 --ping-type=tcpsyn --send-sport=4214 --fromaddr 2001:db8:44::14 --to 2001:db8:45::1
+
+ states=$(mktemp) || exit 1
+ jexec router pfctl -qvss | normalize_pfctl_s > $states
+ nodes=$(mktemp) || exit 1
+ jexec router pfctl -qvvsS | normalize_pfctl_s > $nodes
+
+ echo " === states ==="
+ cat $states
+ echo " === nodes ==="
+ cat $nodes
+ echo " === end === "
+
+ # Order of states in output is not guaranteed, find each one separately.
+ for state_regexp in \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::1\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::2\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, route sticky-address$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::3\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track$' \
+ 'all tcp 2001:db8:45::1\[9\] <- 2001:db8:44::4\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, route sticky-address$' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::11\[4211\] .* 1:0 pkts, 76:0 bytes, rule 4, NAT/RDR sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::12\[4212\] .* 1:0 pkts, 76:0 bytes, rule 5, NAT/RDR sticky-address, route sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::13\[4213\] .* 1:0 pkts, 76:0 bytes, rule 6, limit source-track, NAT/RDR sticky-address' \
+ 'all tcp 2001:db8:45::1\[4242\] \(2001:db8:45::1\[9\]\) <- 2001:db8:44::14\[4214\] .* 1:0 pkts, 76:0 bytes, rule 7, limit source-track, NAT/RDR sticky-address, route sticky-address' \
+ ; do
+ grep -qE "${state_regexp}" $states || atf_fail "State not found for '${state_regexp}'"
+ done
+
+ # Order of source nodes in output is not guaranteed, find each one separately.
+ for node_regexp in \
+ '2001:db8:44::2 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \
+ '2001:db8:44::3 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \
+ '2001:db8:44::4 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \
+ '2001:db8:44::4 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \
+ '2001:db8:44::11 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \
+ '2001:db8:44::12 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \
+ '2001:db8:44::12 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 5, route sticky-address' \
+ '2001:db8:44::13 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \
+ '2001:db8:44::13 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 6, limit source-track' \
+ '2001:db8:44::14 -> 2001:db8:45::1 \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 3, NAT/RDR sticky-address' \
+ '2001:db8:44::14 -> 2001:db8:43::2:1 \( states 1, connections 0, rate 0.0/0s ) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, route sticky-address' \
+ '2001:db8:44::14 -> :: \( states 1, connections 0, rate 0.0/0s \) age [0-9:]+, 1 pkts, 76 bytes, filter rule 7, limit source-track' \
+ ; do
+ grep -qE "${node_regexp}" $nodes || atf_fail "Source node not found for '${node_regexp}'"
+ done
+}
+
+sn_types_pass_cleanup()
+{
+ pft_cleanup
+}
+
atf_init_test_cases()
{
atf_add_test_case "source_track"
+ atf_add_test_case "kill"
+ atf_add_test_case "max_src_conn_rule"
+ atf_add_test_case "max_src_states_rule"
+ atf_add_test_case "max_src_states_global"
+ atf_add_test_case "sn_types_compat"
+ atf_add_test_case "sn_types_pass"
}
diff --git a/tests/sys/netpfil/pf/status.sh b/tests/sys/netpfil/pf/status.sh
new file mode 100644
index 000000000000..bfd916a40c01
--- /dev/null
+++ b/tests/sys/netpfil/pf/status.sh
@@ -0,0 +1,73 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2024 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+. $(atf_get_srcdir)/utils.subr
+
+atf_test_case "basic" "cleanup"
+basic_head()
+{
+ atf_set descr 'Basic get/clear status test case'
+ atf_set require.user root
+}
+
+basic_body()
+{
+ pft_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail one ${epair}a
+ jexec one ifconfig ${epair}a 192.0.2.1/24 up
+ vnet_mkjail two ${epair}b
+ jexec two ifconfig ${epair}b 192.0.2.2/24 up
+
+ jexec one pfctl -e
+ pft_set_rules one "pass"
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore \
+ jexec two ping -c 1 192.0.2.1
+
+ atf_check -s exit:0 -o not-match:'searches[[:space:]]+0' \
+ jexec one pfctl -si
+
+ atf_check -s exit:0 -o ignore -e ignore \
+ jexec one pfctl -Fi
+
+ atf_check -s exit:0 -o match:'searches[[:space:]]+0' \
+ jexec one pfctl -si
+}
+
+basic_cleanup()
+{
+ pft_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "basic"
+}
+
diff --git a/tests/sys/netpfil/pf/syncookie.sh b/tests/sys/netpfil/pf/syncookie.sh
index 8feb2816f589..fad90f3b2618 100644
--- a/tests/sys/netpfil/pf/syncookie.sh
+++ b/tests/sys/netpfil/pf/syncookie.sh
@@ -51,7 +51,7 @@ basic_body()
vnet_mkjail alcatraz ${epair}b
jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
ifconfig ${epair}a 192.0.2.2/24 up
@@ -81,7 +81,7 @@ basic_body()
basic_cleanup()
{
- rm -f inetd-alcatraz.pid
+ rm -f ${PWD}/inetd-alcatraz.pid
pft_cleanup
}
@@ -100,7 +100,7 @@ basic_v6_body()
vnet_mkjail alcatraz ${epair}b
jexec alcatraz ifconfig ${epair}b inet6 2001:db8::1/64 up no_dad
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
ifconfig ${epair}a inet6 2001:db8::2/64 up no_dad
@@ -130,7 +130,6 @@ basic_v6_body()
basic_v6_cleanup()
{
- rm -f inetd-alcatraz.pid
pft_cleanup
}
@@ -157,7 +156,7 @@ forward_body()
jexec srv ifconfig ${epair_out}b 198.51.100.2/24 up
jexec srv route add default 198.51.100.1
- jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
ifconfig ${epair_in}a 192.0.2.2/24 up
@@ -181,7 +180,6 @@ forward_body()
forward_cleanup()
{
- rm -f inetd-alcatraz.pid
pft_cleanup
}
@@ -208,7 +206,7 @@ forward_v6_body()
jexec srv ifconfig ${epair_out}b inet6 2001:db8:1::2/64 up no_dad
jexec srv route -6 add default 2001:db8:1::1
- jexec srv /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec srv /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
ifconfig ${epair_in}a inet6 2001:db8::2/64 up no_dad
@@ -232,7 +230,90 @@ forward_v6_body()
forward_v6_cleanup()
{
- rm -f inetd-alcatraz.pid
+ pft_cleanup
+}
+
+loopback_test()
+{
+ local addr port
+
+ addr=$1
+ port=$2
+
+ # syncookies don't work without state tracking enabled.
+ atf_check -e ignore pfctl -e
+ atf_check pfctl -f - <<__EOF__
+set syncookies always
+pass all keep state
+__EOF__
+
+ # Try to transmit data over a loopback connection.
+ cat <<__EOF__ >in
+Creativity, no.
+__EOF__
+ nc -l $addr $port >out &
+
+ atf_check nc -N $addr $port < in
+
+ atf_check -o file:in cat out
+
+ atf_check -e ignore pfctl -d
+}
+
+atf_test_case "loopback" "cleanup"
+loopback_head()
+{
+ atf_set descr 'Make sure that loopback v4 TCP connections work with syncookies on'
+ atf_set require.user root
+}
+
+loopback_body()
+{
+ local epair
+
+ pft_init
+
+ atf_check ifconfig lo0 127.0.0.1/8
+ atf_check ifconfig lo0 up
+
+ loopback_test 127.0.0.1 8080
+
+ epair=$(vnet_mkepair)
+ atf_check ifconfig ${epair}a inet 192.0.2.1/24
+
+ loopback_test 192.0.2.1 8081
+}
+
+loopback_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "loopback_v6" "cleanup"
+loopback_v6_head()
+{
+ atf_set descr 'Make sure that loopback v6 TCP connections work with syncookies on'
+ atf_set require.user root
+}
+
+loopback_v6_body()
+{
+ local epair
+
+ pft_init
+
+ atf_check ifconfig lo0 up
+
+ loopback_test ::1 8080
+
+ epair=$(vnet_mkepair)
+ atf_check ifconfig ${epair}a inet6 2001:db8::1/64
+
+ loopback_test 2001:db8::1 8081
+}
+
+loopback_v6_cleanup()
+{
pft_cleanup
}
@@ -241,7 +322,7 @@ nostate_head()
{
atf_set descr 'Ensure that we do not create until SYN|ACK'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
nostate_body()
@@ -287,7 +368,7 @@ nostate_v6_head()
{
atf_set descr 'Ensure that we do not create until SYN|ACK'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
nostate_v6_body()
@@ -334,7 +415,7 @@ adaptive_head()
{
atf_set descr 'Adaptive mode test'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
adaptive_body()
@@ -440,7 +521,7 @@ port_reuse_body()
vnet_mkjail alcatraz ${epair}b
vnet_mkjail singsing
jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up
- jexec alcatraz /usr/sbin/inetd -p ${HOME}/inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
ifconfig ${epair}a 192.0.2.2/24 up
@@ -486,6 +567,8 @@ atf_init_test_cases()
atf_add_test_case "basic_v6"
atf_add_test_case "forward"
atf_add_test_case "forward_v6"
+ atf_add_test_case "loopback"
+ atf_add_test_case "loopback_v6"
atf_add_test_case "nostate"
atf_add_test_case "nostate_v6"
atf_add_test_case "adaptive"
diff --git a/tests/sys/netpfil/pf/synproxy.sh b/tests/sys/netpfil/pf/synproxy.sh
index 3b3dc62b8993..617fa6ba2afc 100644
--- a/tests/sys/netpfil/pf/synproxy.sh
+++ b/tests/sys/netpfil/pf/synproxy.sh
@@ -52,7 +52,7 @@ synproxy_body()
jexec singsing ifconfig ${link}b 198.51.100.2/24 up
jexec singsing route add default 198.51.100.1
- jexec singsing /usr/sbin/inetd -p inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf
+ jexec singsing /usr/sbin/inetd -p ${PWD}/inetd-singsing.pid $(atf_get_srcdir)/echo_inetd.conf
jexec alcatraz pfctl -e
pft_set_rules alcatraz "set fail-policy return" \
@@ -74,7 +74,6 @@ synproxy_body()
synproxy_cleanup()
{
- rm -f inetd-singsing.pid
pft_cleanup
}
@@ -94,7 +93,7 @@ local_body()
vnet_mkjail alcatraz ${epair}b
jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
jexec alcatraz pfctl -e
@@ -115,7 +114,6 @@ local_body()
local_cleanup()
{
- rm -f inetd-alcatraz.pid
pft_cleanup
}
@@ -135,7 +133,7 @@ local_v6_body()
vnet_mkjail alcatraz ${epair}b
jexec alcatraz ifconfig ${epair}b inet6 2001:db8:42::2/64 up
- jexec alcatraz /usr/sbin/inetd -p inetd-alcatraz.pid \
+ jexec alcatraz /usr/sbin/inetd -p ${PWD}/inetd-alcatraz.pid \
$(atf_get_srcdir)/echo_inetd.conf
jexec alcatraz pfctl -e
@@ -155,7 +153,6 @@ local_v6_body()
local_v6_cleanup()
{
- rm -f inetd-alcatraz.pid
pft_cleanup
}
diff --git a/tests/sys/netpfil/pf/table.sh b/tests/sys/netpfil/pf/table.sh
index 32943e659bd0..78320375db7c 100644
--- a/tests/sys/netpfil/pf/table.sh
+++ b/tests/sys/netpfil/pf/table.sh
@@ -109,6 +109,268 @@ v6_counters_cleanup()
pft_cleanup
}
+atf_test_case "match_counters" "cleanup"
+match_counters_head()
+{
+ atf_set descr 'Test that counters for tables in match rules work'
+ atf_set require.user root
+}
+
+match_counters_body()
+{
+ pft_init
+
+ epair_send=$(vnet_mkepair)
+ ifconfig ${epair_send}a 192.0.2.1/24 up
+
+ vnet_mkjail alcatraz ${epair_send}b
+ jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up
+ jexec alcatraz pfctl -e
+
+ pft_set_rules alcatraz \
+ "table <foo> counters { 192.0.2.1 }" \
+ "pass all" \
+ "match in from <foo> to any" \
+ "match out from any to <foo>" \
+ "set skip on lo"
+
+ atf_check -s exit:0 -o ignore ping -c 3 192.0.2.2
+
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -T show -vv
+}
+
+match_counters_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "zero_one" "cleanup"
+zero_one_head()
+{
+ atf_set descr 'Test zeroing a single address in a table'
+ atf_set require.user root
+}
+
+pft_cleared_ctime()
+{
+ jexec "$1" pfctl -t "$2" -vvT show | awk -v ip="$3" '
+ ($1 == ip) { m = 1 }
+ ($1 == "Cleared:" && m) {
+ sub("[[:space:]]*Cleared:[[:space:]]*", ""); print; exit }'
+}
+
+ctime_to_unixtime()
+{
+ # NB: it's not TZ=UTC, it's TZ=/etc/localtime
+ date -jf '%a %b %d %H:%M:%S %Y' "$1" '+%s'
+}
+
+zero_one_body()
+{
+ pft_init
+
+ epair_send=$(vnet_mkepair)
+ ifconfig ${epair_send}a 192.0.2.1/24 up
+ ifconfig ${epair_send}a inet alias 192.0.2.3/24
+
+ vnet_mkjail alcatraz ${epair_send}b
+ jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up
+ jexec alcatraz pfctl -e
+
+ pft_set_rules alcatraz \
+ "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \
+ "block all" \
+ "pass in from <foo> to any" \
+ "pass out from any to <foo>" \
+ "set skip on lo"
+
+ atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2
+ atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2
+
+ jexec alcatraz pfctl -t foo -T show -vv
+
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -T show -vv
+
+ local uniq base ts1 ts3
+ uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared`
+ atf_check_equal 1 "$uniq" # time they were added
+
+ base=`pft_cleared_ctime alcatraz foo 192.0.2.1`
+
+ atf_check -s exit:0 -e ignore \
+ jexec alcatraz pfctl -t foo -T zero 192.0.2.3
+
+ ts1=`pft_cleared_ctime alcatraz foo 192.0.2.1`
+ atf_check_equal "$base" "$ts1"
+
+ ts3=`pft_cleared_ctime alcatraz foo 192.0.2.3`
+ atf_check test "$ts1" != "$ts3"
+
+ ts1=`ctime_to_unixtime "$ts1"`
+ ts3=`ctime_to_unixtime "$ts3"`
+ atf_check test $(( "$ts3" - "$ts1" )) -lt 10 # (3 pings * 2) + epsilon
+ atf_check test "$ts1" -lt "$ts3"
+
+ # We now have a zeroed and a non-zeroed counter, so both patterns
+ # should match
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -T show -vv
+}
+
+zero_one_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "zero_all" "cleanup"
+zero_all_head()
+{
+ atf_set descr 'Test zeroing all table entries'
+ atf_set require.user root
+}
+
+zero_all_body()
+{
+ pft_init
+
+ epair_send=$(vnet_mkepair)
+ ifconfig ${epair_send}a 192.0.2.1/24 up
+ ifconfig ${epair_send}a inet alias 192.0.2.3/24
+
+ vnet_mkjail alcatraz ${epair_send}b
+ jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up
+ jexec alcatraz pfctl -e
+
+ pft_set_rules alcatraz \
+ "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \
+ "block all" \
+ "pass in from <foo> to any" \
+ "pass out from any to <foo>" \
+ "set skip on lo"
+
+ atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.1 192.0.2.2
+ atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2
+
+ jexec alcatraz pfctl -t foo -T show -vv
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'Out/Block:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -T show -vv
+
+ atf_check -s exit:0 -e ignore \
+ jexec alcatraz pfctl -t foo -T zero
+
+ jexec alcatraz pfctl -t foo -T show -vv
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -T show -vv
+}
+
+zero_all_cleanup()
+{
+ pft_cleanup
+}
+
+atf_test_case "reset_nonzero" "cleanup"
+reset_nonzero_head()
+{
+ atf_set descr 'Test zeroing an address with non-zero counters'
+ atf_set require.user root
+}
+
+reset_nonzero_body()
+{
+ pft_init
+
+ epair_send=$(vnet_mkepair)
+ ifconfig ${epair_send}a 192.0.2.1/24 up
+ ifconfig ${epair_send}a inet alias 192.0.2.3/24
+
+ vnet_mkjail alcatraz ${epair_send}b
+ jexec alcatraz ifconfig ${epair_send}b 192.0.2.2/24 up
+ jexec alcatraz pfctl -e
+
+ pft_set_rules alcatraz \
+ "table <foo> counters { 192.0.2.1, 192.0.2.3 }" \
+ "table <bar> counters { }" \
+ "block all" \
+ "pass in from <foo> to any" \
+ "pass out from any to <foo>" \
+ "pass on notReallyAnIf from <bar> to <bar>" \
+ "set skip on lo"
+
+ # Nonexisting table can't be reset, following `-T show`.
+ atf_check -o ignore \
+ -s not-exit:0 \
+ -e inline:"pfctl: Table does not exist.\n" \
+ jexec alcatraz pfctl -t nonexistent -T reset
+
+ atf_check -o ignore \
+ -s exit:0 \
+ -e inline:"0/0 stats cleared.\n" \
+ jexec alcatraz pfctl -t bar -T reset
+
+ # No-op is a valid operation.
+ atf_check -s exit:0 \
+ -e inline:"0/2 stats cleared.\n" \
+ jexec alcatraz pfctl -t foo -T reset
+
+ atf_check -s exit:0 -o ignore ping -c 3 -S 192.0.2.3 192.0.2.2
+
+ atf_check -s exit:0 -e ignore \
+ -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -vvT show
+
+ local clrd uniq
+ clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared`
+ uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared`
+ atf_check_equal "$clrd" 2
+ atf_check_equal "$uniq" 1 # time they were added
+
+ atf_check -s exit:0 -e ignore \
+ -e inline:"1/2 stats cleared.\n" \
+ jexec alcatraz pfctl -t foo -T reset
+
+ clrd=`jexec alcatraz pfctl -t foo -vvT show | grep -c Cleared`
+ uniq=`jexec alcatraz pfctl -t foo -vvT show | sort -u | grep -c Cleared`
+ atf_check_equal "$clrd" 2
+ atf_check_equal "$uniq" 2 # 192.0.2.3 should get new timestamp
+
+ atf_check -s exit:0 -e ignore \
+ -o not-match:'In/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o not-match:'Out/Pass:.*'"$TABLE_STATS_NONZERO_REGEXP" \
+ -o match:'In/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ -o match:'Out/Pass:.*'"$TABLE_STATS_ZERO_REGEXP" \
+ jexec alcatraz pfctl -t foo -vvT show
+}
+
+reset_nonzero_cleanup()
+{
+ pft_cleanup
+}
+
atf_test_case "pr251414" "cleanup"
pr251414_head()
{
@@ -324,6 +586,10 @@ atf_init_test_cases()
{
atf_add_test_case "v4_counters"
atf_add_test_case "v6_counters"
+ atf_add_test_case "match_counters"
+ atf_add_test_case "zero_one"
+ atf_add_test_case "zero_all"
+ atf_add_test_case "reset_nonzero"
atf_add_test_case "pr251414"
atf_add_test_case "automatic"
atf_add_test_case "network"
diff --git a/tests/sys/netpfil/pf/tcp.py b/tests/sys/netpfil/pf/tcp.py
new file mode 100644
index 000000000000..53e0658f419c
--- /dev/null
+++ b/tests/sys/netpfil/pf/tcp.py
@@ -0,0 +1,158 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+import sys
+import pytest
+import random
+import socket
+import selectors
+from utils import DelayedSend
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+class TCPClient:
+ def __init__(self, src, dst, sport, dport, sp):
+ self.src = src
+ self.dst = dst
+ self.sport = sport
+ self.dport = dport
+ self.sp = sp
+ self.seq = random.randrange(1, (2**32)-1)
+ self.ack = 0
+
+ def syn(self):
+ syn = self.sp.IP(src=self.src, dst=self.dst) \
+ / self.sp.TCP(sport=self.sport, dport=self.dport, flags="S", seq=self.seq)
+ return syn
+
+ def connect(self):
+ syn = self.syn()
+ r = self.sp.sr1(syn, timeout=5)
+
+ assert r
+ t = r.getlayer(self.sp.TCP)
+ assert t
+ assert t.sport == self.dport
+ assert t.dport == self.sport
+ assert t.flags == "SA"
+
+ self.seq += 1
+ self.ack = t.seq + 1
+ ack = self.sp.IP(src=self.src, dst=self.dst) \
+ / self.sp.TCP(sport=self.sport, dport=self.dport, flags="A", ack=self.ack, seq=self.seq)
+ self.sp.send(ack)
+
+ def send(self, data):
+ length = len(data)
+ pkt = self.sp.IP(src=self.src, dst=self.dst) \
+ / self.sp.TCP(sport=self.sport, dport=self.dport, ack=self.ack, seq=self.seq, flags="") \
+ / self.sp.Raw(data)
+ self.seq += length
+ pkt.show()
+ self.sp.send(pkt)
+
+class TestTcp(VnetTestTemplate):
+ REQUIRED_MODULES = [ "pf" ]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "vnet2": {"ifaces": ["if1"]},
+ "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]},
+ }
+
+ def vnet2_handler(self, vnet):
+ ToolsHelper.print_output("/usr/sbin/arp -s 192.0.2.3 00:01:02:03:04:05")
+ ToolsHelper.print_output("/sbin/pfctl -e")
+ ToolsHelper.pf_rules([
+ "pass"
+ ])
+ ToolsHelper.print_output("/sbin/pfctl -x loud")
+
+ # Start TCP listener
+ sel = selectors.DefaultSelector()
+ t = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ t.bind(("0.0.0.0", 1234))
+ t.listen(100)
+ t.setblocking(False)
+ sel.register(t, selectors.EVENT_READ, data=None)
+
+ while True:
+ events = sel.select(timeout=2)
+ for key, mask in events:
+ sock = key.fileobj
+ if key.data is None:
+ conn, addr = sock.accept()
+ print(f"Accepted connection from {addr}")
+ events = selectors.EVENT_READ | selectors.EVENT_WRITE
+ sel.register(conn, events, data="TCP")
+ else:
+ if mask & selectors.EVENT_READ:
+ recv_data = sock.recv(1024)
+ print(f"Received TCP {recv_data}")
+ ToolsHelper.print_output("/sbin/pfctl -ss -vv")
+ sock.send(recv_data)
+
+ @pytest.mark.require_user("root")
+ @pytest.mark.require_progs(["scapy"])
+ def test_challenge_ack(self):
+ vnet = self.vnet_map["vnet1"]
+ ifname = vnet.iface_alias_map["if1"].name
+
+ # Import in the correct vnet, so at to not confuse Scapy
+ import scapy.all as sp
+
+ a = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
+ a.connect()
+ a.send(b"foo")
+
+ b = TCPClient("192.0.2.3", "192.0.2.2", 1234, 1234, sp)
+ syn = b.syn()
+ syn.show()
+ s = DelayedSend(syn)
+ packets = sp.sniff(iface=ifname, timeout=3)
+ found = False
+ for p in packets:
+ ip = p.getlayer(sp.IP)
+ if not ip:
+ continue
+ tcp = p.getlayer(sp.TCP)
+ if not tcp:
+ continue
+
+ if ip.src != "192.0.2.2":
+ continue
+
+ p.show()
+
+ assert ip.dst == "192.0.2.3"
+ assert tcp.sport == 1234
+ assert tcp.dport == 1234
+ assert tcp.flags == "A"
+
+ # We only expect one
+ assert not found
+ found = True
+
+ assert found
diff --git a/tests/sys/netpfil/pf/tcp.sh b/tests/sys/netpfil/pf/tcp.sh
index 84536480b44e..f6a9ffde1383 100644
--- a/tests/sys/netpfil/pf/tcp.sh
+++ b/tests/sys/netpfil/pf/tcp.sh
@@ -33,12 +33,13 @@ rst_head()
{
atf_set descr 'Check sequence number validation in RST packets'
atf_set require.user root
- atf_set require.progs scapy
+ atf_set require.progs python3 scapy
}
rst_body()
{
pft_init
+ vnet_init_bridge
epair_srv=$(vnet_mkepair)
epair_cl=$(vnet_mkepair)
diff --git a/tests/sys/netpfil/pf/utils.py b/tests/sys/netpfil/pf/utils.py
new file mode 100644
index 000000000000..3d1c1de86aad
--- /dev/null
+++ b/tests/sys/netpfil/pf/utils.py
@@ -0,0 +1,46 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2025 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+import threading
+import time
+
+class DelayedSend(threading.Thread):
+ def __init__(self, packet, sendif=None):
+ threading.Thread.__init__(self)
+ self._packet = packet
+ self._sendif = sendif
+
+ self.start()
+
+ def run(self):
+ import scapy.all as sp
+ time.sleep(1)
+
+ if self._sendif:
+ sp.sendp(self._packet, iface=self._sendif)
+ else:
+ sp.send(self._packet)
+
diff --git a/tests/sys/netpfil/pf/utils.subr b/tests/sys/netpfil/pf/utils.subr
index 25720c1bcb66..6af10e80390d 100644
--- a/tests/sys/netpfil/pf/utils.subr
+++ b/tests/sys/netpfil/pf/utils.subr
@@ -83,6 +83,8 @@ pfsynct_init()
pflog_init()
{
+ pft_init
+
if ! kldstat -q -m pflog; then
atf_skip "This test requires pflog"
fi
@@ -213,8 +215,9 @@ setup_router_server_ipv4()
vnet_mkjail server ${epair_server}b
jexec server ifconfig ${epair_server}b ${net_server_host_server}/${net_server_mask} up
jexec server route add -net ${net_tester} ${net_server_host_router}
- jexec server nc -4l 666 &
- sleep 1 # Give nc time to start and listen
+ inetd_conf=$(mktemp)
+ echo "discard stream tcp nowait root internal" > $inetd_conf
+ jexec server inetd -p ${PWD}/inetd.pid $inetd_conf
}
# Create a bare router jail.
@@ -266,8 +269,9 @@ setup_router_server_ipv6()
vnet_mkjail server ${epair_server}b
jexec server ifconfig ${epair_server}b inet6 ${net_server_host_server}/${net_server_mask} up no_dad
jexec server route add -6 ${net_tester} ${net_server_host_router}
- jexec server nc -6l 666 &
- sleep 1 # Give nc time to start and listen
+ inetd_conf=$(mktemp)
+ echo "discard stream tcp6 nowait root internal" > $inetd_conf
+ jexec server inetd -p ${PWD}/inetd.pid $inetd_conf
}
# Ping the dummy static NDP target.
@@ -297,3 +301,13 @@ ping_server_check_reply()
--replyif ${epair_tester}a \
$params
}
+
+normalize_pfctl_s()
+{
+ # `pfctl -s[rsS]` output is divided into sections. Each rule, state or
+ # source node starts with the beginning of a line and next lines with leading
+ # spaces are various parameters of said rule, state or source node.
+ # Convert it into a single line per entry, and remove multiple spaces,
+ # so that regular expressions for matching them in tests can be simpler.
+ awk '{ if ($0 ~ /^[^ ]/ && NR > 1) print(""); gsub(/ +/, " ", $0); printf("%s", $0); } END {print("");}'
+}
diff --git a/tests/sys/opencrypto/Makefile b/tests/sys/opencrypto/Makefile
index 8be069349aa2..a9c06ef2a93b 100644
--- a/tests/sys/opencrypto/Makefile
+++ b/tests/sys/opencrypto/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/opencrypto
diff --git a/tests/sys/pjdfstest/Makefile b/tests/sys/pjdfstest/Makefile
index 9ad68c2ac6f3..f8a6f1cd6b8d 100644
--- a/tests/sys/pjdfstest/Makefile
+++ b/tests/sys/pjdfstest/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
SUBDIR+= pjdfstest
diff --git a/tests/sys/pjdfstest/Makefile.inc b/tests/sys/pjdfstest/Makefile.inc
index 03efce15f856..cec69b26e149 100644
--- a/tests/sys/pjdfstest/Makefile.inc
+++ b/tests/sys/pjdfstest/Makefile.inc
@@ -1,2 +1 @@
-
.include "${SRCTOP}/tests/Makefile.inc0"
diff --git a/tests/sys/pjdfstest/pjdfstest/Makefile b/tests/sys/pjdfstest/pjdfstest/Makefile
index 30c4713da177..95e6d953d146 100644
--- a/tests/sys/pjdfstest/pjdfstest/Makefile
+++ b/tests/sys/pjdfstest/pjdfstest/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
.PATH: ${SRCTOP}/contrib/pjdfstest
diff --git a/tests/sys/pjdfstest/tests/Makefile b/tests/sys/pjdfstest/tests/Makefile
index c2b5c3d166d5..abe854eb8c35 100644
--- a/tests/sys/pjdfstest/tests/Makefile
+++ b/tests/sys/pjdfstest/tests/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
PJDFSTEST_SRCDIR= ${SRCTOP}/contrib/pjdfstest
diff --git a/tests/sys/pjdfstest/tests/Makefile.inc b/tests/sys/pjdfstest/tests/Makefile.inc
index d3b5cbd3a79b..01b5f23410c8 100644
--- a/tests/sys/pjdfstest/tests/Makefile.inc
+++ b/tests/sys/pjdfstest/tests/Makefile.inc
@@ -1,2 +1 @@
-
.include "../Makefile.inc"
diff --git a/tests/sys/pjdfstest/tests/chflags/Makefile b/tests/sys/pjdfstest/tests/chflags/Makefile
index 308e86e9f533..e0dd497b6807 100644
--- a/tests/sys/pjdfstest/tests/chflags/Makefile
+++ b/tests/sys/pjdfstest/tests/chflags/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/chmod/Makefile b/tests/sys/pjdfstest/tests/chmod/Makefile
index a3e600f4d232..441bec65a357 100644
--- a/tests/sys/pjdfstest/tests/chmod/Makefile
+++ b/tests/sys/pjdfstest/tests/chmod/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/chown/Makefile b/tests/sys/pjdfstest/tests/chown/Makefile
index 1012341c82a4..8138a674f052 100644
--- a/tests/sys/pjdfstest/tests/chown/Makefile
+++ b/tests/sys/pjdfstest/tests/chown/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/ftruncate/Makefile b/tests/sys/pjdfstest/tests/ftruncate/Makefile
index 2f3425423764..30aeb30cb525 100644
--- a/tests/sys/pjdfstest/tests/ftruncate/Makefile
+++ b/tests/sys/pjdfstest/tests/ftruncate/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/granular/Makefile b/tests/sys/pjdfstest/tests/granular/Makefile
index a67a50ede7b2..28ff001d6958 100644
--- a/tests/sys/pjdfstest/tests/granular/Makefile
+++ b/tests/sys/pjdfstest/tests/granular/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/link/Makefile b/tests/sys/pjdfstest/tests/link/Makefile
index 7a890e96aef4..a94c3281a01f 100644
--- a/tests/sys/pjdfstest/tests/link/Makefile
+++ b/tests/sys/pjdfstest/tests/link/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/mkdir/Makefile b/tests/sys/pjdfstest/tests/mkdir/Makefile
index a3e600f4d232..441bec65a357 100644
--- a/tests/sys/pjdfstest/tests/mkdir/Makefile
+++ b/tests/sys/pjdfstest/tests/mkdir/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/mkfifo/Makefile b/tests/sys/pjdfstest/tests/mkfifo/Makefile
index a3e600f4d232..441bec65a357 100644
--- a/tests/sys/pjdfstest/tests/mkfifo/Makefile
+++ b/tests/sys/pjdfstest/tests/mkfifo/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/mknod/Makefile b/tests/sys/pjdfstest/tests/mknod/Makefile
index edfa0847268a..03f178034e85 100644
--- a/tests/sys/pjdfstest/tests/mknod/Makefile
+++ b/tests/sys/pjdfstest/tests/mknod/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/open/Makefile b/tests/sys/pjdfstest/tests/open/Makefile
index e4c656242d0a..12f54ba412d9 100644
--- a/tests/sys/pjdfstest/tests/open/Makefile
+++ b/tests/sys/pjdfstest/tests/open/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/rename/Makefile b/tests/sys/pjdfstest/tests/rename/Makefile
index 913c0d5d2a49..b6a4c06c28a4 100644
--- a/tests/sys/pjdfstest/tests/rename/Makefile
+++ b/tests/sys/pjdfstest/tests/rename/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/rmdir/Makefile b/tests/sys/pjdfstest/tests/rmdir/Makefile
index 19ecd8b0743a..974f7f741825 100644
--- a/tests/sys/pjdfstest/tests/rmdir/Makefile
+++ b/tests/sys/pjdfstest/tests/rmdir/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/symlink/Makefile b/tests/sys/pjdfstest/tests/symlink/Makefile
index a3e600f4d232..441bec65a357 100644
--- a/tests/sys/pjdfstest/tests/symlink/Makefile
+++ b/tests/sys/pjdfstest/tests/symlink/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/truncate/Makefile b/tests/sys/pjdfstest/tests/truncate/Makefile
index 2f3425423764..30aeb30cb525 100644
--- a/tests/sys/pjdfstest/tests/truncate/Makefile
+++ b/tests/sys/pjdfstest/tests/truncate/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/unlink/Makefile b/tests/sys/pjdfstest/tests/unlink/Makefile
index 308e86e9f533..e0dd497b6807 100644
--- a/tests/sys/pjdfstest/tests/unlink/Makefile
+++ b/tests/sys/pjdfstest/tests/unlink/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/pjdfstest/tests/utimensat/Makefile b/tests/sys/pjdfstest/tests/utimensat/Makefile
index 7dd5b6a02b20..16b90be6d6d8 100644
--- a/tests/sys/pjdfstest/tests/utimensat/Makefile
+++ b/tests/sys/pjdfstest/tests/utimensat/Makefile
@@ -1,4 +1,3 @@
-
TAP_TESTS_SH= 00
TAP_TESTS_SH+= 01
TAP_TESTS_SH+= 02
diff --git a/tests/sys/posixshm/Makefile b/tests/sys/posixshm/Makefile
index d7b46cd13867..9b87860fba58 100644
--- a/tests/sys/posixshm/Makefile
+++ b/tests/sys/posixshm/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/posixshm
diff --git a/tests/sys/posixshm/posixshm_test.c b/tests/sys/posixshm/posixshm_test.c
index ade07a118707..55514a5f4bde 100644
--- a/tests/sys/posixshm/posixshm_test.c
+++ b/tests/sys/posixshm/posixshm_test.c
@@ -1190,6 +1190,33 @@ ATF_TC_BODY(accounting, tc)
ATF_REQUIRE(close(fd) == 0);
}
+ATF_TC_WITHOUT_HEAD(mmap_prot);
+ATF_TC_BODY(mmap_prot, tc)
+{
+ void *p;
+ int fd, pagesize;
+
+ ATF_REQUIRE((pagesize = getpagesize()) > 0);
+
+ gen_test_path();
+ fd = shm_open(test_path, O_RDONLY | O_CREAT, 0644);
+ ATF_REQUIRE(fd >= 0);
+
+ p = mmap(NULL, pagesize, PROT_READ, MAP_SHARED, fd, 0);
+ ATF_REQUIRE(p != MAP_FAILED);
+ ATF_REQUIRE(munmap(p, pagesize) == 0);
+ p = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ ATF_REQUIRE_ERRNO(EACCES, p == MAP_FAILED);
+ p = mmap(NULL, pagesize, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0);
+ ATF_REQUIRE(p != MAP_FAILED);
+ ATF_REQUIRE(munmap(p, pagesize) == 0);
+
+ ATF_REQUIRE_MSG(shm_unlink(test_path) == 0,
+ "shm_unlink failed; errno=%d", errno);
+ ATF_REQUIRE_MSG(close(fd) == 0,
+ "close failed; errno=%d", errno);
+}
+
static int
shm_open_large(int psind, int policy, size_t sz)
{
@@ -1920,7 +1947,6 @@ ATF_TC_BODY(largepage_reopen, tc)
ATF_TP_ADD_TCS(tp)
{
-
ATF_TP_ADD_TC(tp, remap_object);
ATF_TP_ADD_TC(tp, rename_from_anon);
ATF_TP_ADD_TC(tp, rename_bad_path_pointer);
@@ -1954,6 +1980,7 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, fallocate);
ATF_TP_ADD_TC(tp, fspacectl);
ATF_TP_ADD_TC(tp, accounting);
+ ATF_TP_ADD_TC(tp, mmap_prot);
ATF_TP_ADD_TC(tp, largepage_basic);
ATF_TP_ADD_TC(tp, largepage_config);
ATF_TP_ADD_TC(tp, largepage_mmap);
diff --git a/tests/sys/sound/Makefile b/tests/sys/sound/Makefile
new file mode 100644
index 000000000000..74a0765a0540
--- /dev/null
+++ b/tests/sys/sound/Makefile
@@ -0,0 +1,13 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/sound
+
+ATF_TESTS_C+= pcm_read_write
+ATF_TESTS_C+= sndstat
+
+CFLAGS+= -I${SRCTOP}/sys
+LDFLAGS+= -lnv
+
+CFLAGS.pcm_read_write.c+= -Wno-cast-align
+
+.include <bsd.test.mk>
diff --git a/tests/sys/sound/pcm_read_write.c b/tests/sys/sound/pcm_read_write.c
new file mode 100644
index 000000000000..a77b953a78a0
--- /dev/null
+++ b/tests/sys/sound/pcm_read_write.c
@@ -0,0 +1,262 @@
+/*-
+ * Copyright (c) 2025 Florian Walpen
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * These tests exercise conversion functions of the sound module, used to read
+ * pcm samples from a buffer, and write pcm samples to a buffer. The test cases
+ * are non-exhaustive, but should detect systematic errors in conversion of the
+ * various sample formats supported. In particular, the test cases establish
+ * correctness independent of the machine's endianness, making them suitable to
+ * check for architecture-specific problems.
+ */
+
+#include <sys/types.h>
+#include <sys/soundcard.h>
+
+#include <atf-c.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <dev/sound/pcm/sound.h>
+#include <dev/sound/pcm/pcm.h>
+#include <dev/sound/pcm/g711.h>
+
+/* Generic test data, with buffer content matching the sample values. */
+static struct afmt_test_data {
+ const char *label;
+ uint8_t buffer[4];
+ size_t size;
+ int format;
+ intpcm_t value;
+ _Static_assert((sizeof(intpcm_t) == 4),
+ "Test data assumes 32bit, adjust negative values to new size.");
+} const afmt_tests[] = {
+ /* 8 bit sample formats. */
+ {"s8_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_S8, 0x00000001},
+ {"s8_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_S8, 0xffffff81},
+ {"u8_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_U8, 0xffffff81},
+ {"u8_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_U8, 0x00000001},
+
+ /* 16 bit sample formats. */
+ {"s16le_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_S16_LE, 0x00000201},
+ {"s16le_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_S16_LE, 0xffff8281},
+ {"s16be_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_S16_BE, 0x00000102},
+ {"s16be_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_S16_BE, 0xffff8182},
+ {"u16le_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_U16_LE, 0xffff8201},
+ {"u16le_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_U16_LE, 0x00000281},
+ {"u16be_1", {0x01, 0x02, 0x00, 0x00}, 2, AFMT_U16_BE, 0xffff8102},
+ {"u16be_2", {0x81, 0x82, 0x00, 0x00}, 2, AFMT_U16_BE, 0x00000182},
+
+ /* 24 bit sample formats. */
+ {"s24le_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_S24_LE, 0x00030201},
+ {"s24le_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_S24_LE, 0xff838281},
+ {"s24be_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_S24_BE, 0x00010203},
+ {"s24be_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_S24_BE, 0xff818283},
+ {"u24le_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_U24_LE, 0xff830201},
+ {"u24le_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_U24_LE, 0x00038281},
+ {"u24be_1", {0x01, 0x02, 0x03, 0x00}, 3, AFMT_U24_BE, 0xff810203},
+ {"u24be_2", {0x81, 0x82, 0x83, 0x00}, 3, AFMT_U24_BE, 0x00018283},
+
+ /* 32 bit sample formats. */
+ {"s32le_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_S32_LE, 0x04030201},
+ {"s32le_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_S32_LE, 0x84838281},
+ {"s32be_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_S32_BE, 0x01020304},
+ {"s32be_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_S32_BE, 0x81828384},
+ {"u32le_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_U32_LE, 0x84030201},
+ {"u32le_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_U32_LE, 0x04838281},
+ {"u32be_1", {0x01, 0x02, 0x03, 0x04}, 4, AFMT_U32_BE, 0x81020304},
+ {"u32be_2", {0x81, 0x82, 0x83, 0x84}, 4, AFMT_U32_BE, 0x01828384},
+
+ /* 32 bit floating point sample formats. */
+ {"f32le_1", {0x00, 0x00, 0x00, 0x3f}, 4, AFMT_F32_LE, 0x40000000},
+ {"f32le_2", {0x00, 0x00, 0x00, 0xbf}, 4, AFMT_F32_LE, 0xc0000000},
+ {"f32be_1", {0x3f, 0x00, 0x00, 0x00}, 4, AFMT_F32_BE, 0x40000000},
+ {"f32be_2", {0xbf, 0x00, 0x00, 0x00}, 4, AFMT_F32_BE, 0xc0000000},
+
+ /* u-law and A-law sample formats. */
+ {"mulaw_1", {0x01, 0x00, 0x00, 0x00}, 1, AFMT_MU_LAW, 0xffffff87},
+ {"mulaw_2", {0x81, 0x00, 0x00, 0x00}, 1, AFMT_MU_LAW, 0x00000079},
+ {"alaw_1", {0x2a, 0x00, 0x00, 0x00}, 1, AFMT_A_LAW, 0xffffff83},
+ {"alaw_2", {0xab, 0x00, 0x00, 0x00}, 1, AFMT_A_LAW, 0x00000079}
+};
+
+/* Normalize sample values in strictly correct (but slow) c. */
+static intpcm_t
+local_normalize(intpcm_t value, int val_bits, int norm_bits)
+{
+ int32_t divisor;
+ intpcm_t remainder;
+
+ /* Avoid undefined or implementation defined behavior. */
+ if (val_bits < norm_bits)
+ /* Multiply instead of left shift (value may be negative). */
+ return (value * (1 << (norm_bits - val_bits)));
+ else if (val_bits > norm_bits) {
+ divisor = (1 << (val_bits - norm_bits));
+ /* Positive remainder, to discard lowest bits from value. */
+ remainder = value % divisor;
+ remainder = (remainder + divisor) % divisor;
+ /* Divide instead of right shift (value may be negative). */
+ return ((value - remainder) / divisor);
+ }
+ return value;
+}
+
+/* Restrict magnitude of sample value to 24bit for 32bit calculations. */
+static intpcm_t
+local_calc_limit(intpcm_t value, int val_bits)
+{
+ /*
+ * When intpcm32_t is defined to be 32bit, calculations for mixing and
+ * volume changes use 32bit integers instead of 64bit. To get some
+ * headroom for calculations, 32bit sample values are restricted to
+ * 24bit magnitude in that case. Also avoid implementation defined
+ * behavior here.
+ */
+ if (sizeof(intpcm32_t) == (32 / 8) && val_bits == 32)
+ return (local_normalize(value, 32, 24));
+ return value;
+}
+
+ATF_TC(pcm_read);
+ATF_TC_HEAD(pcm_read, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Read and verify different pcm sample formats.");
+}
+ATF_TC_BODY(pcm_read, tc)
+{
+ const struct afmt_test_data *test;
+ uint8_t src[4];
+ intpcm_t expected, result;
+ size_t i;
+
+ for (i = 0; i < nitems(afmt_tests); i++) {
+ test = &afmt_tests[i];
+
+ /* Copy byte representation, fill with distinctive pattern. */
+ memset(src, 0x66, sizeof(src));
+ memcpy(src, test->buffer, test->size);
+
+ /* Read sample at format magnitude. */
+ expected = test->value;
+ result = pcm_sample_read(src, test->format);
+ ATF_CHECK_MSG(result == expected,
+ "pcm_read[\"%s\"].value: expected=0x%08x, result=0x%08x",
+ test->label, expected, result);
+
+ /* Read sample at format magnitude, for calculations. */
+ expected = local_calc_limit(test->value, test->size * 8);
+ result = pcm_sample_read_calc(src, test->format);
+ ATF_CHECK_MSG(result == expected,
+ "pcm_read[\"%s\"].calc: expected=0x%08x, result=0x%08x",
+ test->label, expected, result);
+
+ /* Read sample at full 32 bit magnitude. */
+ expected = local_normalize(test->value, test->size * 8, 32);
+ result = pcm_sample_read_norm(src, test->format);
+ ATF_CHECK_MSG(result == expected,
+ "pcm_read[\"%s\"].norm: expected=0x%08x, result=0x%08x",
+ test->label, expected, result);
+ }
+}
+
+ATF_TC(pcm_write);
+ATF_TC_HEAD(pcm_write, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Write and verify different pcm sample formats.");
+}
+ATF_TC_BODY(pcm_write, tc)
+{
+ const struct afmt_test_data *test;
+ uint8_t expected[4];
+ uint8_t dst[4];
+ intpcm_t value;
+ size_t i;
+
+ for (i = 0; i < nitems(afmt_tests); i++) {
+ test = &afmt_tests[i];
+
+ /* Write sample of format specific magnitude. */
+ memcpy(expected, test->buffer, sizeof(expected));
+ memset(dst, 0x00, sizeof(dst));
+ value = test->value;
+ pcm_sample_write(dst, value, test->format);
+ ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0,
+ "pcm_write[\"%s\"].value: "
+ "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, "
+ "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label,
+ expected[0], expected[1], expected[2], expected[3],
+ dst[0], dst[1], dst[2], dst[3]);
+
+ /* Write sample of format specific, calculation magnitude. */
+ memcpy(expected, test->buffer, sizeof(expected));
+ memset(dst, 0x00, sizeof(dst));
+ value = local_calc_limit(test->value, test->size * 8);
+ if (value != test->value) {
+ /*
+ * 32 bit sample was reduced to 24 bit resolution
+ * for calculation, least significant byte is lost.
+ */
+ if (test->format & AFMT_BIGENDIAN)
+ expected[3] = 0x00;
+ else
+ expected[0] = 0x00;
+ }
+ pcm_sample_write_calc(dst, value, test->format);
+ ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0,
+ "pcm_write[\"%s\"].calc: "
+ "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, "
+ "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label,
+ expected[0], expected[1], expected[2], expected[3],
+ dst[0], dst[1], dst[2], dst[3]);
+
+ /* Write normalized sample of full 32 bit magnitude. */
+ memcpy(expected, test->buffer, sizeof(expected));
+ memset(dst, 0x00, sizeof(dst));
+ value = local_normalize(test->value, test->size * 8, 32);
+ pcm_sample_write_norm(dst, value, test->format);
+ ATF_CHECK_MSG(memcmp(dst, expected, sizeof(dst)) == 0,
+ "pcm_write[\"%s\"].norm: "
+ "expected={0x%02x, 0x%02x, 0x%02x, 0x%02x}, "
+ "result={0x%02x, 0x%02x, 0x%02x, 0x%02x}, ", test->label,
+ expected[0], expected[1], expected[2], expected[3],
+ dst[0], dst[1], dst[2], dst[3]);
+ }
+}
+
+ATF_TC(pcm_format_bits);
+ATF_TC_HEAD(pcm_format_bits, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify bit width of different pcm sample formats.");
+}
+ATF_TC_BODY(pcm_format_bits, tc)
+{
+ const struct afmt_test_data *test;
+ size_t bits;
+ size_t i;
+
+ for (i = 0; i < nitems(afmt_tests); i++) {
+ test = &afmt_tests[i];
+
+ /* Check bit width determined for given sample format. */
+ bits = AFMT_BIT(test->format);
+ ATF_CHECK_MSG(bits == test->size * 8,
+ "format_bits[%zu].size: expected=%zu, result=%zu",
+ i, test->size * 8, bits);
+ }
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pcm_read);
+ ATF_TP_ADD_TC(tp, pcm_write);
+ ATF_TP_ADD_TC(tp, pcm_format_bits);
+
+ return atf_no_error();
+}
diff --git a/tests/sys/sound/sndstat.c b/tests/sys/sound/sndstat.c
new file mode 100644
index 000000000000..ed292b570429
--- /dev/null
+++ b/tests/sys/sound/sndstat.c
@@ -0,0 +1,395 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 The FreeBSD Foundation
+ *
+ * This software was developed by Christos Margiolis <christos@FreeBSD.org>
+ * under sponsorship from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/linker.h>
+#include <sys/nv.h>
+#include <sys/sndstat.h>
+#include <sys/soundcard.h>
+
+#include <atf-c.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+static void
+load_dummy(void)
+{
+ if (kldload("snd_dummy.ko") < 0 && errno != EEXIST)
+ atf_tc_skip("snd_dummy.ko not found");
+}
+
+ATF_TC(sndstat_nv);
+ATF_TC_HEAD(sndstat_nv, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "/dev/sndstat nvlist test");
+}
+
+ATF_TC_BODY(sndstat_nv, tc)
+{
+ nvlist_t *nvl;
+ const nvlist_t * const *di;
+ const nvlist_t * const *cdi;
+ struct sndstioc_nv_arg arg;
+ size_t nitems, nchans, i, j;
+ int fd, rc, pchan, rchan;
+
+ load_dummy();
+
+ if ((fd = open("/dev/sndstat", O_RDONLY)) < 0)
+ atf_tc_skip("/dev/sndstat not found, load sound(4)");
+
+ rc = ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL);
+ ATF_REQUIRE_EQ(rc, 0);
+
+ arg.nbytes = 0;
+ arg.buf = NULL;
+ rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg);
+ ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#1) failed");
+
+ arg.buf = malloc(arg.nbytes);
+ ATF_REQUIRE(arg.buf != NULL);
+
+ rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg);
+ ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#2) failed");
+
+ nvl = nvlist_unpack(arg.buf, arg.nbytes, 0);
+ ATF_REQUIRE(nvl != NULL);
+
+ if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS))
+ atf_tc_skip("no soundcards attached");
+
+ di = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &nitems);
+ for (i = 0; i < nitems; i++) {
+#define NV(type, item) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(di[i], SNDST_DSPS_ ## item), \
+ "SNDST_DSPS_" #item " does not exist"); \
+ nvlist_get_ ## type (di[i], SNDST_DSPS_ ## item); \
+} while (0)
+ NV(string, NAMEUNIT);
+ NV(bool, FROM_USER);
+ NV(string, DEVNODE);
+ NV(string, DESC);
+ NV(string, PROVIDER);
+ NV(number, PCHAN);
+ NV(number, RCHAN);
+#undef NV
+
+ /* Cannot asign using the macro. */
+ pchan = nvlist_get_number(di[i], SNDST_DSPS_PCHAN);
+ rchan = nvlist_get_number(di[i], SNDST_DSPS_RCHAN);
+
+ if (pchan && !nvlist_exists(di[i], SNDST_DSPS_INFO_PLAY))
+ atf_tc_fail("playback channel list empty");
+ if (rchan && !nvlist_exists(di[i], SNDST_DSPS_INFO_REC))
+ atf_tc_fail("recording channel list empty");
+
+#define NV(type, mode, item) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(di[i], \
+ SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item), \
+ "SNDST_DSPS_INFO_" #item " does not exist"); \
+ nvlist_get_ ## type (nvlist_get_nvlist(di[i], \
+ SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item); \
+} while (0)
+ if (pchan) {
+ NV(number, PLAY, MIN_RATE);
+ NV(number, PLAY, MAX_RATE);
+ NV(number, PLAY, FORMATS);
+ NV(number, PLAY, MIN_CHN);
+ NV(number, PLAY, MAX_CHN);
+ }
+ if (rchan) {
+ NV(number, REC, MIN_RATE);
+ NV(number, REC, MAX_RATE);
+ NV(number, REC, FORMATS);
+ NV(number, REC, MIN_CHN);
+ NV(number, REC, MAX_CHN);
+ }
+#undef NV
+
+ if (!nvlist_exists(di[i], SNDST_DSPS_PROVIDER_INFO))
+ continue;
+
+#define NV(type, item) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(di[i], \
+ SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_ ## item), \
+ "SNDST_DSPS_SOUND4_" #item " does not exist"); \
+ nvlist_get_ ## type (nvlist_get_nvlist(di[i], \
+ SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_ ## item); \
+} while (0)
+ NV(number, UNIT);
+ NV(string, STATUS);
+ NV(bool, BITPERFECT);
+ NV(bool, PVCHAN);
+ NV(number, PVCHANRATE);
+ NV(number, PVCHANFORMAT);
+ NV(bool, RVCHAN);
+ NV(number, PVCHANRATE);
+ NV(number, PVCHANFORMAT);
+#undef NV
+
+ if (!nvlist_exists(nvlist_get_nvlist(di[i],
+ SNDST_DSPS_PROVIDER_INFO), SNDST_DSPS_SOUND4_CHAN_INFO))
+ atf_tc_fail("channel info list empty");
+
+ cdi = nvlist_get_nvlist_array(
+ nvlist_get_nvlist(di[i], SNDST_DSPS_PROVIDER_INFO),
+ SNDST_DSPS_SOUND4_CHAN_INFO, &nchans);
+ for (j = 0; j < nchans; j++) {
+#define NV(type, item) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(cdi[j], SNDST_DSPS_SOUND4_CHAN_ ## item), \
+ "SNDST_DSPS_SOUND4_CHAN_" #item " does not exist"); \
+ nvlist_get_ ## type (cdi[j], SNDST_DSPS_SOUND4_CHAN_ ## item); \
+} while (0)
+ NV(string, NAME);
+ NV(string, PARENTCHAN);
+ NV(number, UNIT);
+ NV(number, CAPS);
+ NV(number, LATENCY);
+ NV(number, RATE);
+ NV(number, FORMAT);
+ NV(number, PID);
+ NV(string, COMM);
+ NV(number, INTR);
+ NV(number, XRUNS);
+ NV(number, FEEDCNT);
+ NV(number, LEFTVOL);
+ NV(number, RIGHTVOL);
+ NV(number, HWBUF_FORMAT);
+ NV(number, HWBUF_RATE);
+ NV(number, HWBUF_SIZE);
+ NV(number, HWBUF_BLKSZ);
+ NV(number, HWBUF_BLKCNT);
+ NV(number, HWBUF_FREE);
+ NV(number, HWBUF_READY);
+ NV(number, SWBUF_FORMAT);
+ NV(number, SWBUF_RATE);
+ NV(number, SWBUF_SIZE);
+ NV(number, SWBUF_BLKSZ);
+ NV(number, SWBUF_BLKCNT);
+ NV(number, SWBUF_FREE);
+ NV(number, SWBUF_READY);
+ NV(string, FEEDERCHAIN);
+#undef NV
+ }
+ }
+
+ free(arg.buf);
+ nvlist_destroy(nvl);
+ close(fd);
+}
+
+#define UDEV_PROVIDER "sndstat_udev"
+#define UDEV_NAMEUNIT "sndstat_udev"
+#define UDEV_DEVNODE "sndstat_udev"
+#define UDEV_DESC "Test Device"
+#define UDEV_PCHAN 1
+#define UDEV_RCHAN 1
+#define UDEV_MIN_RATE 8000
+#define UDEV_MAX_RATE 96000
+#define UDEV_FORMATS (AFMT_S16_NE | AFMT_S24_NE | AFMT_S32_NE)
+#define UDEV_MIN_CHN 1
+#define UDEV_MAX_CHN 2
+
+ATF_TC(sndstat_udev);
+ATF_TC_HEAD(sndstat_udev, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "/dev/sndstat userdev interface test");
+}
+
+ATF_TC_BODY(sndstat_udev, tc)
+{
+ nvlist_t *nvl, *di, *dichild;
+ const nvlist_t * const *rdi;
+ struct sndstioc_nv_arg arg;
+ const char *str;
+ size_t nitems, i;
+ int fd, rc, pchan, rchan, n;
+
+ load_dummy();
+
+ if ((fd = open("/dev/sndstat", O_RDWR)) < 0)
+ atf_tc_skip("/dev/sndstat not found, load sound(4)");
+
+ nvl = nvlist_create(0);
+ ATF_REQUIRE(nvl != NULL);
+
+ di = nvlist_create(0);
+ ATF_REQUIRE(di != NULL);
+
+ dichild = nvlist_create(0);
+ ATF_REQUIRE(dichild != NULL);
+
+ nvlist_add_string(di, SNDST_DSPS_PROVIDER, UDEV_PROVIDER);
+ nvlist_add_string(di, SNDST_DSPS_NAMEUNIT, UDEV_NAMEUNIT);
+ nvlist_add_string(di, SNDST_DSPS_DESC, UDEV_DESC);
+ nvlist_add_string(di, SNDST_DSPS_DEVNODE, UDEV_DEVNODE);
+ nvlist_add_number(di, SNDST_DSPS_PCHAN, UDEV_PCHAN);
+ nvlist_add_number(di, SNDST_DSPS_RCHAN, UDEV_RCHAN);
+
+ nvlist_add_number(dichild, SNDST_DSPS_INFO_MIN_RATE, UDEV_MIN_RATE);
+ nvlist_add_number(dichild, SNDST_DSPS_INFO_MAX_RATE, UDEV_MAX_RATE);
+ nvlist_add_number(dichild, SNDST_DSPS_INFO_FORMATS, UDEV_FORMATS);
+ nvlist_add_number(dichild, SNDST_DSPS_INFO_MIN_CHN, UDEV_MIN_CHN);
+ nvlist_add_number(dichild, SNDST_DSPS_INFO_MAX_CHN, UDEV_MAX_CHN);
+
+ nvlist_add_nvlist(di, SNDST_DSPS_INFO_PLAY, dichild);
+ nvlist_add_nvlist(di, SNDST_DSPS_INFO_REC, dichild);
+
+ nvlist_append_nvlist_array(nvl, SNDST_DSPS, di);
+ ATF_REQUIRE_EQ(nvlist_error(nvl), 0);
+
+ arg.buf = nvlist_pack(nvl, &arg.nbytes);
+ ATF_REQUIRE_MSG(arg.buf != NULL, "failed to pack nvlist");
+
+ rc = ioctl(fd, SNDSTIOC_ADD_USER_DEVS, &arg);
+ free(arg.buf);
+ ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_ADD_USER_DEVS) failed");
+
+ nvlist_destroy(di);
+ nvlist_destroy(dichild);
+ nvlist_destroy(nvl);
+
+ /* Read back registered values. */
+ rc = ioctl(fd, SNDSTIOC_REFRESH_DEVS, NULL);
+ ATF_REQUIRE_EQ(rc, 0);
+
+ arg.nbytes = 0;
+ arg.buf = NULL;
+ rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg);
+ ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#1) failed");
+
+ arg.buf = malloc(arg.nbytes);
+ ATF_REQUIRE(arg.buf != NULL);
+
+ rc = ioctl(fd, SNDSTIOC_GET_DEVS, &arg);
+ ATF_REQUIRE_EQ_MSG(rc, 0, "ioctl(SNDSTIOC_GET_DEVS#2) failed");
+
+ nvl = nvlist_unpack(arg.buf, arg.nbytes, 0);
+ ATF_REQUIRE(nvl != NULL);
+
+ if (nvlist_empty(nvl) || !nvlist_exists(nvl, SNDST_DSPS))
+ atf_tc_skip("no soundcards attached");
+
+ rdi = nvlist_get_nvlist_array(nvl, SNDST_DSPS, &nitems);
+ for (i = 0; i < nitems; i++) {
+#define NV(type, item, var) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(rdi[i], SNDST_DSPS_ ## item), \
+ "SNDST_DSPS_" #item " does not exist"); \
+ var = nvlist_get_ ## type (rdi[i], SNDST_DSPS_ ## item); \
+} while (0)
+ /* Search for our device. */
+ NV(string, NAMEUNIT, str);
+ if (strcmp(str, UDEV_NAMEUNIT) == 0)
+ break;
+ }
+ if (i == nitems)
+ atf_tc_fail("userland device %s not found", UDEV_NAMEUNIT);
+
+ NV(string, NAMEUNIT, str);
+ ATF_CHECK(strcmp(str, UDEV_NAMEUNIT) == 0);
+
+ NV(bool, FROM_USER, n);
+ ATF_CHECK(n);
+
+ NV(string, DEVNODE, str);
+ ATF_CHECK(strcmp(str, UDEV_DEVNODE) == 0);
+
+ NV(string, DESC, str);
+ ATF_CHECK(strcmp(str, UDEV_DESC) == 0);
+
+ NV(string, PROVIDER, str);
+ ATF_CHECK(strcmp(str, UDEV_PROVIDER) == 0);
+
+ NV(number, PCHAN, pchan);
+ ATF_CHECK(pchan == UDEV_PCHAN);
+ if (pchan && !nvlist_exists(rdi[i], SNDST_DSPS_INFO_PLAY))
+ atf_tc_fail("playback channel list empty");
+
+ NV(number, RCHAN, rchan);
+ ATF_CHECK(rchan == UDEV_RCHAN);
+ if (rchan && !nvlist_exists(rdi[i], SNDST_DSPS_INFO_REC))
+ atf_tc_fail("recording channel list empty");
+#undef NV
+
+#define NV(type, mode, item, var) do { \
+ ATF_REQUIRE_MSG(nvlist_exists(nvlist_get_nvlist(rdi[i], \
+ SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item), \
+ "SNDST_DSPS_INFO_" #item " does not exist"); \
+ var = nvlist_get_ ## type (nvlist_get_nvlist(rdi[i], \
+ SNDST_DSPS_INFO_ ## mode), SNDST_DSPS_INFO_ ## item); \
+} while (0)
+ if (pchan) {
+ NV(number, PLAY, MIN_RATE, n);
+ ATF_CHECK(n == UDEV_MIN_RATE);
+
+ NV(number, PLAY, MAX_RATE, n);
+ ATF_CHECK(n == UDEV_MAX_RATE);
+
+ NV(number, PLAY, FORMATS, n);
+ ATF_CHECK(n == UDEV_FORMATS);
+
+ NV(number, PLAY, MIN_CHN, n);
+ ATF_CHECK(n == UDEV_MIN_CHN);
+
+ NV(number, PLAY, MAX_CHN, n);
+ ATF_CHECK(n == UDEV_MAX_CHN);
+ }
+ if (rchan) {
+ NV(number, REC, MIN_RATE, n);
+ ATF_CHECK(n == UDEV_MIN_RATE);
+
+ NV(number, REC, MAX_RATE, n);
+ ATF_CHECK(n == UDEV_MAX_RATE);
+
+ NV(number, REC, FORMATS, n);
+ ATF_CHECK(n == UDEV_FORMATS);
+
+ NV(number, REC, MIN_CHN, n);
+ ATF_CHECK(n == UDEV_MIN_CHN);
+
+ NV(number, REC, MAX_CHN, n);
+ ATF_CHECK(n == UDEV_MAX_CHN);
+ }
+#undef NV
+
+ free(arg.buf);
+ nvlist_destroy(nvl);
+ close(fd);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, sndstat_nv);
+ ATF_TP_ADD_TC(tp, sndstat_udev);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/sys/Makefile b/tests/sys/sys/Makefile
index e9c9bfef7dbd..a1b4e3234e1c 100644
--- a/tests/sys/sys/Makefile
+++ b/tests/sys/sys/Makefile
@@ -1,4 +1,3 @@
-
.include <bsd.compiler.mk>
TESTSDIR= ${TESTSBASE}/sys/sys
@@ -6,7 +5,9 @@ TESTSDIR= ${TESTSBASE}/sys/sys
ATF_TESTS_C= arb_test \
bitset_test \
bitstring_test \
+ buf_ring_test \
qmath_test \
+ queue_test \
rb_test \
splay_test \
time_test
diff --git a/tests/sys/sys/buf_ring_test.c b/tests/sys/sys/buf_ring_test.c
new file mode 100644
index 000000000000..611fb23788cc
--- /dev/null
+++ b/tests/sys/sys/buf_ring_test.c
@@ -0,0 +1,180 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 Arm Ltd
+ */
+
+#include <sys/types.h>
+
+#include <machine/cpu.h>
+#include <machine/cpufunc.h>
+
+#include <errno.h>
+#include <stdint.h>
+
+#include <atf-c.h>
+
+static void critical_enter(void);
+static void critical_exit(void);
+
+#include <sys/buf_ring.h>
+
+static void
+critical_enter(void)
+{
+}
+
+static void
+critical_exit(void)
+{
+}
+
+static void *
+buf_ring_dequeue_peek(struct buf_ring *br)
+{
+ void *val;
+
+ val = buf_ring_peek(br);
+ if (val != NULL)
+ buf_ring_advance_sc(br);
+ return (val);
+}
+
+static void *
+buf_ring_dequeue_peek_clear_sc(struct buf_ring *br)
+{
+ void *val;
+
+ val = buf_ring_peek_clear_sc(br);
+ if (val != NULL)
+ buf_ring_advance_sc(br);
+ return (val);
+}
+
+#define MC_SC_TEST(dequeue_func) \
+ATF_TC_WITHOUT_HEAD(dequeue_func); \
+ATF_TC_BODY(dequeue_func, tc) \
+{ \
+ struct buf_ring *br; \
+ \
+ br = buf_ring_alloc(4); \
+ ATF_REQUIRE_MSG(br != NULL, "buf_ring_alloc returned NULL"); \
+ \
+ ATF_REQUIRE(dequeue_func(br) == NULL); \
+ ATF_REQUIRE(buf_ring_count(br) == 0); \
+ ATF_REQUIRE(!buf_ring_full(br)); \
+ ATF_REQUIRE(buf_ring_empty(br)); \
+ \
+ /* Try filling the buf_ring */ \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)1) == 0); \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)2) == 0); \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)3) == 0); \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)4) == ENOBUFS); \
+ \
+ ATF_REQUIRE(buf_ring_count(br) == 3); \
+ ATF_REQUIRE(buf_ring_full(br)); \
+ ATF_REQUIRE(!buf_ring_empty(br)); \
+ \
+ /* Partially empty it */ \
+ ATF_REQUIRE(dequeue_func(br) == (void *)1); \
+ ATF_REQUIRE(dequeue_func(br) == (void *)2); \
+ \
+ ATF_REQUIRE(buf_ring_count(br) == 1); \
+ ATF_REQUIRE(!buf_ring_full(br)); \
+ ATF_REQUIRE(!buf_ring_empty(br)); \
+ \
+ /* Add more items */ \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)5) == 0); \
+ ATF_REQUIRE(buf_ring_count(br) == 2); \
+ \
+ /* Finish emptying it */ \
+ ATF_REQUIRE(dequeue_func(br) == (void *)3); \
+ ATF_REQUIRE(dequeue_func(br) == (void *)5); \
+ ATF_REQUIRE(dequeue_func(br) == NULL); \
+ \
+ ATF_REQUIRE(buf_ring_count(br) == 0); \
+ ATF_REQUIRE(!buf_ring_full(br)); \
+ ATF_REQUIRE(buf_ring_empty(br)); \
+ \
+ for (uintptr_t i = 0; i < 8; i++) { \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 100)) == 0); \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 200)) == 0); \
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)(i + 300)) == 0); \
+ ATF_REQUIRE(buf_ring_count(br) == 3); \
+ ATF_REQUIRE(dequeue_func(br) == (void *)(i + 100)); \
+ ATF_REQUIRE(dequeue_func(br) == (void *)(i + 200)); \
+ ATF_REQUIRE(dequeue_func(br) == (void *)(i + 300)); \
+ \
+ ATF_REQUIRE(!buf_ring_full(br)); \
+ ATF_REQUIRE(buf_ring_empty(br)); \
+ } \
+ \
+ buf_ring_free(br); \
+}
+
+MC_SC_TEST(buf_ring_dequeue_sc)
+MC_SC_TEST(buf_ring_dequeue_mc)
+MC_SC_TEST(buf_ring_dequeue_peek)
+MC_SC_TEST(buf_ring_dequeue_peek_clear_sc)
+
+ATF_TC_WITHOUT_HEAD(overflow);
+ATF_TC_BODY(overflow, tc)
+{
+ struct buf_ring *br;
+
+ br = buf_ring_alloc(4);
+ ATF_REQUIRE_MSG(br != NULL, "buf_ring_alloc returned NULL");
+
+ br->br_prod_head = br->br_cons_head = br->br_prod_tail =
+ br->br_cons_tail = UINT32_MAX - 1;
+ ATF_REQUIRE(buf_ring_count(br) == 0);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(buf_ring_empty(br));
+
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)1) == 0);
+ ATF_REQUIRE(buf_ring_count(br) == 1);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(!buf_ring_empty(br));
+
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)2) == 0);
+ ATF_REQUIRE(buf_ring_count(br) == 2);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(!buf_ring_empty(br));
+
+ ATF_REQUIRE(buf_ring_enqueue(br, (void *)3) == 0);
+ ATF_REQUIRE(buf_ring_count(br) == 3);
+ ATF_REQUIRE(buf_ring_full(br));
+ ATF_REQUIRE(!buf_ring_empty(br));
+
+ ATF_REQUIRE(br->br_prod_head == 1);
+ ATF_REQUIRE(br->br_prod_tail == 1);
+ ATF_REQUIRE(br->br_cons_head == UINT32_MAX - 1);
+ ATF_REQUIRE(br->br_cons_tail == UINT32_MAX - 1);
+
+ ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)1);
+ ATF_REQUIRE(buf_ring_count(br) == 2);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(!buf_ring_empty(br));
+
+ ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)2);
+ ATF_REQUIRE(buf_ring_count(br) == 1);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(!buf_ring_empty(br));
+
+ ATF_REQUIRE(buf_ring_dequeue_sc(br) == (void *)3);
+ ATF_REQUIRE(buf_ring_count(br) == 0);
+ ATF_REQUIRE(!buf_ring_full(br));
+ ATF_REQUIRE(buf_ring_empty(br));
+
+ buf_ring_free(br);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, buf_ring_dequeue_sc);
+ ATF_TP_ADD_TC(tp, buf_ring_dequeue_mc);
+ ATF_TP_ADD_TC(tp, buf_ring_dequeue_peek);
+ ATF_TP_ADD_TC(tp, buf_ring_dequeue_peek_clear_sc);
+ ATF_TP_ADD_TC(tp, overflow);
+ return (atf_no_error());
+}
diff --git a/tests/sys/sys/queue_test.c b/tests/sys/sys/queue_test.c
new file mode 100644
index 000000000000..7f8738751b85
--- /dev/null
+++ b/tests/sys/sys/queue_test.c
@@ -0,0 +1,293 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2025 The FreeBSD Foundation
+ *
+ * This software was developed by Olivier Certner <olce@FreeBSD.org> at
+ * Kumacom SARL under sponsorship from the FreeBSD Foundation.
+ */
+
+#include <sys/types.h>
+#define QUEUE_MACRO_DEBUG_ASSERTIONS
+#include <sys/queue.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <atf-c.h>
+
+/*
+ * General utilities.
+ */
+#define DIAG(fmt, ...) do { \
+ fprintf(stderr, "%s(): " fmt "\n", __func__, ##__VA_ARGS__); \
+} while (0)
+
+/*
+ * Common definitions and utilities.
+ *
+ * 'type' should be tailq, stailq, list or slist. 'TYPE' is 'type' in
+ * uppercase.
+ */
+
+#define QUEUE_TESTS_COMMON(type, TYPE) \
+/* \
+ * Definitions and utilities. \
+ */ \
+ \
+struct type ## _id_elem { \
+ TYPE ## _ENTRY(type ## _id_elem) ie_entry; \
+ u_int ie_id; \
+}; \
+ \
+TYPE ## _HEAD(type ## _ids, type ## _id_elem); \
+ \
+static void \
+type ## _check(const struct type ## _ids *const type, \
+ const u_int nb, const u_int id_shift); \
+ \
+/* \
+ * Creates a tailq/list with 'nb' elements with contiguous IDs \
+ * in ascending order starting at 'id_shift'. \
+ */ \
+static struct type ## _ids * \
+type ## _create(const u_int nb, const u_int id_shift) \
+{ \
+ struct type ## _ids *const type = \
+ malloc(sizeof(*type)); \
+ \
+ ATF_REQUIRE_MSG(type != NULL, \
+ "Cannot malloc " #type " head"); \
+ \
+ TYPE ## _INIT(type); \
+ for (u_int i = 0; i < nb; ++i) { \
+ struct type ## _id_elem *const e = \
+ malloc(sizeof(*e)); \
+ \
+ ATF_REQUIRE_MSG(e != NULL, \
+ "Cannot malloc " #type " element %u", i); \
+ e->ie_id = nb - 1 - i + id_shift; \
+ TYPE ## _INSERT_HEAD(type, e, ie_entry); \
+ } \
+ \
+ DIAG("Created " #type " %p with %u elements", \
+ type, nb); \
+ type ## _check(type, nb, id_shift); \
+ return (type); \
+} \
+ \
+/* Performs no check. */ \
+static void \
+type ## _destroy(struct type ## _ids *const type) \
+{ \
+ struct type ## _id_elem *e, *tmp_e; \
+ \
+ DIAG("Destroying " #type" %p", type); \
+ TYPE ## _FOREACH_SAFE(e, type, ie_entry, \
+ tmp_e) { \
+ free(e); \
+ } \
+ free(type); \
+} \
+ \
+ \
+/* Checks that some tailq/list is as produced by *_create(). */ \
+static void \
+type ## _check(const struct type ## _ids *const type, \
+ const u_int nb, const u_int id_shift) \
+{ \
+ struct type ## _id_elem *e; \
+ u_int i = 0; \
+ \
+ TYPE ## _FOREACH(e, type, ie_entry) { \
+ ATF_REQUIRE_MSG(i + 1 <= nb, \
+ #type " %p has more than %u elements", \
+ type, nb); \
+ ATF_REQUIRE_MSG(e->ie_id == i + id_shift, \
+ #type " %p element %p: Found ID %u, " \
+ "expected %u", \
+ type, e, e->ie_id, i + id_shift); \
+ ++i; \
+ } \
+ ATF_REQUIRE_MSG(i == nb, \
+ #type " %p has only %u elements, expected %u", \
+ type, i, nb); \
+} \
+ \
+/* Returns NULL if not enough elements. */ \
+static struct type ## _id_elem * \
+type ## _nth(const struct type ## _ids *const type, \
+ const u_int idx) \
+{ \
+ struct type ## _id_elem *e; \
+ u_int i = 0; \
+ \
+ TYPE ## _FOREACH(e, type, ie_entry) { \
+ if (i == idx) { \
+ DIAG(#type " %p has element %p " \
+ "(ID %u) at index %u", \
+ type, e, e->ie_id, idx); \
+ return (e); \
+ } \
+ ++i; \
+ } \
+ DIAG(#type " %p: Only %u elements, no index %u", \
+ type, i, idx); \
+ return (NULL); \
+} \
+ \
+/* \
+ * Tests. \
+ */ \
+ \
+ATF_TC(type ## _split_after_and_concat); \
+ATF_TC_HEAD(type ## _split_after_and_concat, tc) \
+{ \
+ atf_tc_set_md_var(tc, "descr", \
+ "Test " #TYPE "_SPLIT_AFTER() followed by " \
+ #TYPE "_CONCAT()"); \
+} \
+ATF_TC_BODY(type ## _split_after_and_concat, tc) \
+{ \
+ struct type ## _ids *const type = \
+ type ## _create(100, 0); \
+ struct type ## _ids rest; \
+ struct type ## _id_elem *e; \
+ \
+ e = type ## _nth(type, 49); \
+ TYPE ## _SPLIT_AFTER(type, e, &rest, ie_entry); \
+ type ## _check(type, 50, 0); \
+ type ## _check(&rest, 50, 50); \
+ QUEUE_TESTS_ ## TYPE ## _CONCAT(type, &rest); \
+ ATF_REQUIRE_MSG(TYPE ## _EMPTY(&rest), \
+ "'rest' not empty after concat"); \
+ type ## _check(type, 100, 0); \
+ type ## _destroy(type); \
+}
+
+#define QUEUE_TESTS_CHECK_REVERSED(type, TYPE) \
+/* \
+ * Checks that some tailq/list is reversed. \
+ */ \
+static void \
+type ## _check_reversed(const struct type ## _ids *const type, \
+ const u_int nb, const u_int id_shift) \
+{ \
+ struct type ## _id_elem *e; \
+ u_int i = 0; \
+ \
+ TYPE ## _FOREACH(e, type, ie_entry) { \
+ const u_int expected_id = nb - 1 - i + id_shift; \
+ \
+ ATF_REQUIRE_MSG(i < nb, \
+ #type " %p has more than %u elements", \
+ type, nb); \
+ ATF_REQUIRE_MSG(e->ie_id == expected_id, \
+ #type " %p element %p, idx %u: Found ID %u, " \
+ "expected %u", \
+ type, e, i, e->ie_id, expected_id); \
+ ++i; \
+ } \
+ ATF_REQUIRE_MSG(i == nb, \
+ #type " %p has only %u elements, expected %u", \
+ type, i, nb); \
+}
+
+/*
+ * Paper over the *_CONCAT() signature differences.
+ */
+
+#define QUEUE_TESTS_TAILQ_CONCAT(first, second) \
+ TAILQ_CONCAT(first, second, ie_entry)
+
+#define QUEUE_TESTS_LIST_CONCAT(first, second) \
+ LIST_CONCAT(first, second, list_id_elem, ie_entry)
+
+#define QUEUE_TESTS_STAILQ_CONCAT(first, second) \
+ STAILQ_CONCAT(first, second)
+
+#define QUEUE_TESTS_SLIST_CONCAT(first, second) \
+ SLIST_CONCAT(first, second, slist_id_elem, ie_entry)
+
+/*
+ * ATF test registration.
+ */
+
+#define QUEUE_TESTS_REGISTRATION(tp, type) \
+ ATF_TP_ADD_TC(tp, type ## _split_after_and_concat)
+
+/*
+ * Macros defining print functions.
+ *
+ * They are currently not used in the tests above, but are useful for debugging.
+ */
+
+#define QUEUE_TESTS_TQ_PRINT(type, hfp) \
+ static void \
+ type ## _print(const struct type ## _ids *const type) \
+ { \
+ printf(#type " %p: " __STRING(hfp ## _first) \
+ " = %p, " __STRING(hfp ## _last) " = %p\n", \
+ type, type->hfp ## _first, type->hfp ## _last); \
+ }
+
+#define QUEUE_TESTS_L_PRINT(type, hfp) \
+ static void \
+ type ## _print(const struct type ## _ids *const type) \
+ { \
+ printf(#type " %p: " __STRING(hfp ## _first) " = %p\n", \
+ type, type->hfp ## _first); \
+ }
+
+
+/*
+ * Meat.
+ */
+
+/* Common tests. */
+QUEUE_TESTS_COMMON(tailq, TAILQ);
+QUEUE_TESTS_COMMON(list, LIST);
+QUEUE_TESTS_COMMON(stailq, STAILQ);
+QUEUE_TESTS_COMMON(slist, SLIST);
+
+/* STAILQ_REVERSE(). */
+QUEUE_TESTS_CHECK_REVERSED(stailq, STAILQ);
+ATF_TC(stailq_reverse);
+ATF_TC_HEAD(stailq_reverse, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Test STAILQ_REVERSE");
+}
+ATF_TC_BODY(stailq_reverse, tc)
+{
+ const u_int size = 100;
+ struct stailq_ids *const stailq = stailq_create(size, 0);
+ struct stailq_ids *const empty_stailq = stailq_create(0, 0);
+ const struct stailq_id_elem *last;
+
+ stailq_check(stailq, size, 0);
+ STAILQ_REVERSE(stailq, stailq_id_elem, ie_entry);
+ stailq_check_reversed(stailq, size, 0);
+ last = STAILQ_LAST(stailq, stailq_id_elem, ie_entry);
+ ATF_REQUIRE_MSG(last->ie_id == 0,
+ "Last element of stailq %p has id %u, expected 0",
+ stailq, last->ie_id);
+ stailq_destroy(stailq);
+
+ STAILQ_REVERSE(empty_stailq, stailq_id_elem, ie_entry);
+ stailq_check(empty_stailq, 0, 0);
+ stailq_destroy(empty_stailq);
+}
+
+/*
+ * Main.
+ */
+ATF_TP_ADD_TCS(tp)
+{
+ QUEUE_TESTS_REGISTRATION(tp, tailq);
+ QUEUE_TESTS_REGISTRATION(tp, list);
+ QUEUE_TESTS_REGISTRATION(tp, stailq);
+ QUEUE_TESTS_REGISTRATION(tp, slist);
+ ATF_TP_ADD_TC(tp, stailq_reverse);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/vfs/Makefile b/tests/sys/vfs/Makefile
index 9670ec52674b..e25282b2c70a 100644
--- a/tests/sys/vfs/Makefile
+++ b/tests/sys/vfs/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/vfs
diff --git a/tests/sys/vm/Makefile b/tests/sys/vm/Makefile
index d63610ed272a..8be4b8666fdf 100644
--- a/tests/sys/vm/Makefile
+++ b/tests/sys/vm/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/vm
diff --git a/tests/sys/vm/mmap_test.c b/tests/sys/vm/mmap_test.c
index e5f4a81a7858..6bc30f73ca95 100644
--- a/tests/sys/vm/mmap_test.c
+++ b/tests/sys/vm/mmap_test.c
@@ -295,14 +295,82 @@ ATF_TC_BODY(mmap__write_only, tc)
munmap(p, pagesize);
}
-ATF_TP_ADD_TCS(tp)
+ATF_TC_WITHOUT_HEAD(mmap__maxprot_basic);
+ATF_TC_BODY(mmap__maxprot_basic, tc)
+{
+ void *p;
+ int error, pagesize;
+
+ ATF_REQUIRE((pagesize = getpagesize()) > 0);
+
+ p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ),
+ MAP_ANON, -1, 0);
+ ATF_REQUIRE(p != MAP_FAILED);
+
+ error = mprotect(p, pagesize, PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_EXEC);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+
+ ATF_REQUIRE(munmap(p, pagesize) == 0);
+}
+
+/* Make sure that PROT_MAX applies as expected to mappings of shm objects */
+ATF_TC_WITHOUT_HEAD(mmap__maxprot_shm);
+ATF_TC_BODY(mmap__maxprot_shm, tc)
{
+ void *p;
+ int error, fd, pagesize;
+
+ ATF_REQUIRE((pagesize = getpagesize()) > 0);
+ fd = shm_open(SHM_ANON, O_RDWR, 0644);
+ ATF_REQUIRE(fd >= 0);
+
+ error = ftruncate(fd, pagesize);
+ ATF_REQUIRE(error == 0);
+
+ p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ),
+ MAP_PRIVATE, fd, 0);
+ ATF_REQUIRE(p != MAP_FAILED);
+
+ error = mprotect(p, pagesize, PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_EXEC);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+
+ ATF_REQUIRE(munmap(p, pagesize) == 0);
+
+ /* Again, this time with a shared mapping. */
+ p = mmap(NULL, pagesize, PROT_READ | PROT_MAX(PROT_READ),
+ MAP_SHARED, fd, 0);
+ ATF_REQUIRE(p != MAP_FAILED);
+
+ error = mprotect(p, pagesize, PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_WRITE);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+ error = mprotect(p, pagesize, PROT_READ | PROT_EXEC);
+ ATF_REQUIRE_ERRNO(EACCES, error == -1);
+
+ ATF_REQUIRE(munmap(p, pagesize) == 0);
+
+ ATF_REQUIRE(close(fd) == 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
ATF_TP_ADD_TC(tp, mmap__map_at_zero);
ATF_TP_ADD_TC(tp, mmap__bad_arguments);
ATF_TP_ADD_TC(tp, mmap__dev_zero_private);
ATF_TP_ADD_TC(tp, mmap__dev_zero_shared);
ATF_TP_ADD_TC(tp, mmap__write_only);
+ ATF_TP_ADD_TC(tp, mmap__maxprot_basic);
+ ATF_TP_ADD_TC(tp, mmap__maxprot_shm);
return (atf_no_error());
}
diff --git a/tests/sys/vm/soxstack/Makefile b/tests/sys/vm/soxstack/Makefile
index c0b8ef095bce..f9f3bd55b50a 100644
--- a/tests/sys/vm/soxstack/Makefile
+++ b/tests/sys/vm/soxstack/Makefile
@@ -1,4 +1,4 @@
-
+PACKAGE= tests
SHLIB= soxstack
SHLIB_NAME= libsoxstack.so
SHLIB_MAJOR= 1
diff --git a/tests/sys/vm/soxstack/soxstack.c b/tests/sys/vm/soxstack/soxstack.c
index ac7c9cf03746..ecb672c1b6dc 100644
--- a/tests/sys/vm/soxstack/soxstack.c
+++ b/tests/sys/vm/soxstack/soxstack.c
@@ -23,7 +23,7 @@ checkstack(void)
struct kinfo_vmentry *freep, *kve;
struct kinfo_proc *p;
struct procstat *prstat;
- uint64_t stack;
+ uintptr_t stack;
int i, cnt;
prstat = procstat_open_sysctl();
@@ -33,7 +33,7 @@ checkstack(void)
freep = procstat_getvmmap(prstat, p, &cnt);
assert(freep != NULL);
- stack = (uint64_t)&i;
+ stack = (uintptr_t)&i;
for (i = 0; i < cnt; i++) {
kve = &freep[i];
if (stack < kve->kve_start || stack > kve->kve_end)
diff --git a/tests/sys/vm/stack/Makefile b/tests/sys/vm/stack/Makefile
index 4279e4f48b8f..06ffcf4d4b4a 100644
--- a/tests/sys/vm/stack/Makefile
+++ b/tests/sys/vm/stack/Makefile
@@ -1,4 +1,3 @@
-
PACKAGE= tests
TESTSDIR= ${TESTSBASE}/sys/vm/stack
diff --git a/tests/sys/vmm/vmm_cred_jail.sh b/tests/sys/vmm/vmm_cred_jail.sh
index 4ead7fdd897d..5b02b5dc0b42 100644
--- a/tests/sys/vmm/vmm_cred_jail.sh
+++ b/tests/sys/vmm/vmm_cred_jail.sh
@@ -38,7 +38,7 @@ vmm_cred_jail_host_head()
}
vmm_cred_jail_host_body()
{
- if ! kldstat -qn vmm; then
+ if ! -c /dev/vmmctl; then
atf_skip "vmm is not loaded"
fi
bhyvectl --vm=testvm --create
@@ -59,7 +59,7 @@ vmm_cred_jail_other_head()
}
vmm_cred_jail_other_body()
{
- if ! kldstat -qn vmm; then
+ if ! -c /dev/vmmctl; then
atf_skip "vmm is not loaded"
fi
vmm_mkjail myjail1