aboutsummaryrefslogtreecommitdiff
path: root/tests/sys/netinet
diff options
context:
space:
mode:
Diffstat (limited to 'tests/sys/netinet')
-rw-r--r--tests/sys/netinet/Makefile60
-rw-r--r--tests/sys/netinet/Makefile.depend17
-rwxr-xr-xtests/sys/netinet/arp.sh273
-rw-r--r--tests/sys/netinet/broadcast.c196
-rw-r--r--tests/sys/netinet/carp.py69
-rwxr-xr-xtests/sys/netinet/carp.sh593
-rwxr-xr-xtests/sys/netinet/divert.sh151
-rwxr-xr-xtests/sys/netinet/fibs.sh72
-rw-r--r--tests/sys/netinet/fibs_multibind_test.c755
-rw-r--r--tests/sys/netinet/fibs_test.sh830
-rwxr-xr-xtests/sys/netinet/forward.sh325
-rw-r--r--tests/sys/netinet/igmp.py157
-rw-r--r--tests/sys/netinet/ip6_v4mapped_test.c398
-rw-r--r--tests/sys/netinet/ip_reass_test.c384
-rw-r--r--tests/sys/netinet/libalias/1_instance.c119
-rw-r--r--tests/sys/netinet/libalias/2_natout.c547
-rw-r--r--tests/sys/netinet/libalias/3_natin.c381
-rw-r--r--tests/sys/netinet/libalias/Makefile31
-rw-r--r--tests/sys/netinet/libalias/perf.c308
-rw-r--r--tests/sys/netinet/libalias/util.c123
-rw-r--r--tests/sys/netinet/libalias/util.h137
-rwxr-xr-xtests/sys/netinet/lpm.sh178
-rw-r--r--tests/sys/netinet/multicast-receive.c134
-rw-r--r--tests/sys/netinet/multicast-send.c97
-rwxr-xr-xtests/sys/netinet/multicast.sh205
-rwxr-xr-xtests/sys/netinet/output.sh593
-rwxr-xr-xtests/sys/netinet/redirect.py86
-rwxr-xr-xtests/sys/netinet/redirect.sh110
-rw-r--r--tests/sys/netinet/so_reuseport_lb_test.c719
-rw-r--r--tests/sys/netinet/socket_afinet.c599
-rw-r--r--tests/sys/netinet/tcp_connect_port_test.c331
-rw-r--r--tests/sys/netinet/tcp_hpts_test.py4
-rw-r--r--tests/sys/netinet/tcp_implied_connect.c80
-rw-r--r--tests/sys/netinet/tcp_md5_getsockopt.c134
-rw-r--r--tests/sys/netinet/tcp_user_cookie.c110
-rw-r--r--tests/sys/netinet/udp_bindings.c249
-rw-r--r--tests/sys/netinet/udp_dontroute.c136
-rw-r--r--tests/sys/netinet/udp_io.c140
38 files changed, 9831 insertions, 0 deletions
diff --git a/tests/sys/netinet/Makefile b/tests/sys/netinet/Makefile
new file mode 100644
index 000000000000..9739221676ce
--- /dev/null
+++ b/tests/sys/netinet/Makefile
@@ -0,0 +1,60 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/netinet
+BINDIR= ${TESTSDIR}
+
+TESTS_SUBDIRS+= libalias
+
+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 \
+ carp \
+ divert \
+ fibs \
+ fibs_test \
+ forward \
+ lpm \
+ multicast \
+ output \
+ redirect
+
+ATF_TESTS_PYTEST+= carp.py
+ATF_TESTS_PYTEST+= igmp.py
+ATF_TESTS_PYTEST+= tcp_hpts_test.py
+
+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"
+
+PROGS= udp_dontroute tcp_user_cookie multicast-send multicast-receive
+
+${PACKAGE}FILES+= redirect.py
+
+${PACKAGE}FILESMODE_redirect.py=0555
+
+MAN=
+
+.include <bsd.test.mk>
diff --git a/tests/sys/netinet/Makefile.depend b/tests/sys/netinet/Makefile.depend
new file mode 100644
index 000000000000..ec18d8c500c6
--- /dev/null
+++ b/tests/sys/netinet/Makefile.depend
@@ -0,0 +1,17 @@
+# Autogenerated - do NOT edit!
+
+DIRDEPS = \
+ gnu/lib/csu \
+ include \
+ include/arpa \
+ include/xlocale \
+ lib/${CSU_DIR} \
+ lib/libc \
+ lib/libcompiler_rt \
+
+
+.include <dirdeps.mk>
+
+.if ${DEP_RELDIR} == ${_DEP_RELDIR}
+# local dependencies - needed for -jN in clean tree
+.endif
diff --git a/tests/sys/netinet/arp.sh b/tests/sys/netinet/arp.sh
new file mode 100755
index 000000000000..df5dbc50ffa1
--- /dev/null
+++ b/tests/sys/netinet/arp.sh
@@ -0,0 +1,273 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2021 Alexander V. Chernikov
+#
+# 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 "arp_add_success" "cleanup"
+arp_add_success_head() {
+ atf_set descr 'Test static arp record addition'
+ atf_set require.user root
+}
+
+arp_add_success_body() {
+
+ vnet_init
+
+ jname="v4t-arp_add_success"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ jexec ${jname} ifconfig ${epair0}a inet 198.51.100.1/24
+
+ atf_check jexec ${jname} arp -s 198.51.100.2 90:10:00:01:02:03
+
+ atf_check -o match:"\? \(198.51.100.2\) at 90:10:00:01:02:03 on ${epair0}a permanent" jexec ${jname} arp -ni ${epair0}a 198.51.100.2
+}
+
+arp_add_success_cleanup() {
+ vnet_cleanup
+}
+
+
+atf_test_case "arp_del_success" "cleanup"
+arp_del_success_head() {
+ atf_set descr 'Test arp record deletion'
+ atf_set require.user root
+}
+
+arp_del_success_body() {
+
+ vnet_init
+
+ jname="v4t-arp_del_success"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname} ${epair0}a
+
+ jexec ${jname} ifconfig ${epair0}a inet 198.51.100.1/24
+
+ jexec ${jname} ping -c1 -t1 198.51.100.2
+
+ atf_check -o match:"198.51.100.2 \(198.51.100.2\) deleted" jexec ${jname} arp -nd 198.51.100.2
+ atf_check -s exit:1 -o match:"198.51.100.2 \(198.51.100.2\) -- no entry" jexec ${jname} arp -n 198.51.100.2
+}
+
+arp_del_success_cleanup() {
+ vnet_cleanup
+}
+
+atf_test_case "pending_delete_if" "cleanup"
+pending_delete_if_head() {
+ atf_set descr 'Test having pending link layer lookups on interface delete'
+ atf_set require.user root
+}
+
+pending_delete_if_body() {
+ vnet_init
+
+ jname="arp_pending_delete_if"
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}b up
+
+ vnet_mkjail ${jname} ${epair}a
+ jexec ${jname} ifconfig ${epair}a 198.51.100.1/24
+ for i in `seq 2 200`
+ do
+ jexec ${jname} ping 198.51.100.${i} &
+ done
+
+ # Give the ping processes time to send their ARP requests
+ sleep 1
+
+ jexec ${jname} arp -an
+ jexec ${jname} killall ping
+
+ # Delete the interface. Test failure panics the machine.
+ ifconfig ${epair}b destroy
+}
+
+pending_delete_if_cleanup() {
+ vnet_cleanup
+}
+
+
+atf_test_case "arp_lookup_host" "cleanup"
+arp_lookup_host_head() {
+ atf_set descr 'Test looking up a specific host'
+ atf_set require.user root
+}
+
+arp_lookup_host_body() {
+
+ vnet_init
+
+ jname="v4t-arp_lookup_host"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname}a ${epair0}a
+ vnet_mkjail ${jname}b ${epair0}b
+
+ ipa=198.51.100.1
+ ipb=198.51.100.2
+
+ atf_check -o ignore \
+ ifconfig -j ${jname}a ${epair0}a inet ${ipa}/24
+ atf_check -o ignore \
+ ifconfig -j ${jname}b ${epair0}b inet ${ipb}/24
+
+ # get jail b's MAC address
+ eth="$(ifconfig -j ${jname}b ${epair0}b |
+ sed -nE "s/^\tether ([0-9a-f:]*)$/\1/p")"
+
+ # no entry yet
+ atf_check -s exit:1 -o match:"\(${ipb}\) -- no entry" \
+ jexec ${jname}a arp -n ${ipb}
+
+ # now ping jail b from jail a
+ atf_check -o ignore \
+ jexec ${jname}a ping -c1 ${ipb}
+
+ # should be populated
+ atf_check -o match:"\(${ipb}\) at $eth on ${epair0}a" \
+ jexec ${jname}a arp -n ${ipb}
+
+}
+
+arp_lookup_host_cleanup() {
+ vnet_cleanup
+}
+
+
+atf_test_case "static" "cleanup"
+static_head() {
+ atf_set descr 'Test arp -s/-S works'
+ atf_set require.user root
+}
+
+static_body() {
+
+ vnet_init
+
+ jname="v4t-arp_static_host"
+
+ epair0=$(vnet_mkepair)
+
+ vnet_mkjail ${jname}a ${epair0}a
+ vnet_mkjail ${jname}b ${epair0}b
+
+ 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 |
+ sed -nE "s/^\tether ([0-9a-f:]*)$/\1/p")"
+
+ # Expected outputs
+ permanent=\
+"? (${ipb}) at 00:00:00:00:00:00 on ${epair0}a permanent [ethernet]\n"
+ temporary_re=\
+"\? \(${ipb_re}\) at ${eth} on ${epair0}a expires in ${max_age} seconds \[ethernet\]"
+ deleted=\
+"${ipb} (${ipb}) deleted\n"
+
+ # first check -s
+ atf_check jexec ${jname}a arp -s ${ipb} 0:0:0:0:0:0
+ # the jail B ifconfig will send gratuitous ARP that will trigger A
+ atf_check ifconfig -j ${jname}b ${epair0}b inet ${ipb}/24
+ atf_check -o "inline:${permanent}" jexec ${jname}a arp -n ${ipb}
+ if [ $(sysctl -n net.link.ether.inet.log_arp_permanent_modify) -ne 0 ];
+ then
+ msg=$(dmesg | tail -n 1)
+ atf_check_equal "${msg}" \
+"arp: ${eth} attempts to modify permanent entry for ${ipb} on ${epair0}a"
+ fi
+
+ # 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 "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}" \
+ jexec ${jname}a arp -S ${ipb} 0:0:0:0:0:0
+ atf_check -o "inline:${permanent}" jexec ${jname}a arp -n ${ipb}
+}
+
+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()
+{
+
+ atf_add_test_case "arp_add_success"
+ atf_add_test_case "arp_del_success"
+ 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
new file mode 100644
index 000000000000..e35c9470d035
--- /dev/null
+++ b/tests/sys/netinet/carp.py
@@ -0,0 +1,69 @@
+import pytest
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+
+sc = None
+
+
+def filter_f(x):
+ ip = x.getlayer(sc.IP)
+ if not ip:
+ return False
+
+ return ip.proto == 112
+
+
+class TestCarp(VnetTestTemplate):
+ REQUIRED_MODULES = ["carp"]
+ TOPOLOGY = {
+ "vnet1": {"ifaces": ["if1"]},
+ "if1": {"prefixes4": [("192.0.2.1/24", "192.0.2.2/24")]},
+ }
+
+ def setup_method(self, method):
+ global sc
+ if sc is None:
+ import scapy.all as _sc
+
+ sc = _sc
+ super().setup_method(method)
+
+ @classmethod
+ def check_carp_src_mac(self, pkts):
+ for p in pkts:
+ if not filter_f(p):
+ continue
+
+ print("Packet src mac {}".format(p.src))
+
+ 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"
+
+ if1 = self.vnet.iface_alias_map["if1"]
+
+ ToolsHelper.print_output(
+ "ifconfig {} add vhid 1 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)
+
+ @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
new file mode 100755
index 000000000000..568d2beaf914
--- /dev/null
+++ b/tests/sys/netinet/carp.sh
@@ -0,0 +1,593 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 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)/../common/vnet.subr
+
+is_master()
+{
+ jail=$1
+ itf=$2
+
+ jexec ${jail} ifconfig ${itf} | grep -E '(carp|vrrp)' | grep MASTER
+}
+
+wait_for_carp()
+{
+ jail1=$1
+ itf1=$2
+ jail2=$3
+ itf2=$4
+
+ while [ -z "$(is_master ${jail1} ${itf1})" ] &&
+ [ -z "$(is_master ${jail2} ${itf2})" ]; do
+ sleep 1
+ done
+
+ if [ -n "$(is_master ${jail1} ${itf1})" ] &&
+ [ -n "$(is_master ${jail2} ${itf2})" ]; then
+ atf_fail "Both jails are master"
+ fi
+}
+
+carp_init()
+{
+ if ! kldstat -q -m carp; then
+ atf_skip "This test requires carp"
+ fi
+
+ vnet_init
+}
+
+atf_test_case "basic_v4" "cleanup"
+basic_v4_head()
+{
+ atf_set descr 'Basic CARP test (IPv4)'
+ atf_set require.user root
+}
+
+basic_v4_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail carp_basic_v4_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail carp_basic_v4_two ${epair_one}b
+ vnet_mkjail carp_basic_v4_three ${epair_two}b
+
+ jexec carp_basic_v4_one ifconfig ${bridge} 192.0.2.4/29 up
+ jexec carp_basic_v4_one ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec carp_basic_v4_one ifconfig ${epair_one}a up
+ jexec carp_basic_v4_one ifconfig ${epair_two}a up
+
+ jexec carp_basic_v4_two ifconfig ${epair_one}b 192.0.2.202/29 up
+ jexec carp_basic_v4_two ifconfig ${epair_one}b add vhid 1 192.0.2.1/29
+
+ jexec carp_basic_v4_three ifconfig ${epair_two}b 192.0.2.203/29 up
+ jexec carp_basic_v4_three ifconfig ${epair_two}b add vhid 1 \
+ 192.0.2.1/29
+
+ wait_for_carp carp_basic_v4_two ${epair_one}b \
+ carp_basic_v4_three ${epair_two}b
+
+ atf_check -s exit:0 -o ignore jexec carp_basic_v4_one \
+ ping -c 3 192.0.2.1
+}
+
+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()
+{
+ atf_set descr 'Unicast CARP test (IPv4)'
+ atf_set require.user root
+}
+
+unicast_v4_body()
+{
+ carp_init
+
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ 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 sysctl net.inet.ip.forwarding=1
+ 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 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.224 192.0.2.1/32
+
+ 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/32
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore jexec carp_uni_v4_two \
+ 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
+
+ # 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()
+{
+ jexec carp_uni_v4_one killall routed
+ jexec carp_uni_v4_two killall routed
+ jexec carp_uni_v4_three killall routed
+ vnet_cleanup
+}
+
+atf_test_case "basic_v6" "cleanup"
+basic_v6_head()
+{
+ atf_set descr 'Basic CARP test (IPv6)'
+ atf_set require.user root
+}
+
+basic_v6_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail carp_basic_v6_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail carp_basic_v6_two ${epair_one}b
+ vnet_mkjail carp_basic_v6_three ${epair_two}b
+
+ jexec carp_basic_v6_one ifconfig ${bridge} inet6 2001:db8::0:4/64 up \
+ no_dad
+ jexec carp_basic_v6_one ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec carp_basic_v6_one ifconfig ${epair_one}a up
+ jexec carp_basic_v6_one ifconfig ${epair_two}a up
+
+ jexec carp_basic_v6_two ifconfig ${epair_one}b inet6 \
+ 2001:db8::1:2/64 up no_dad
+ jexec carp_basic_v6_two ifconfig ${epair_one}b inet6 add vhid 1 \
+ 2001:db8::0:1/64
+
+ jexec carp_basic_v6_three ifconfig ${epair_two}b inet6 2001:db8::1:3/64 up no_dad
+ jexec carp_basic_v6_three ifconfig ${epair_two}b inet6 add vhid 1 \
+ 2001:db8::0:1/64
+
+ wait_for_carp carp_basic_v6_two ${epair_one}b \
+ carp_basic_v6_three ${epair_two}b
+
+ atf_check -s exit:0 -o ignore jexec carp_basic_v6_one \
+ ping -6 -c 3 2001:db8::0:1
+}
+
+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()
+{
+ atf_set descr 'Unicast CARP test (IPv6)'
+ atf_set require.user root
+}
+
+unicast_v6_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail carp_uni_v6_one ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail carp_uni_v6_two ${epair_one}b
+ vnet_mkjail carp_uni_v6_three ${epair_two}b
+
+ jexec carp_uni_v6_one sysctl net.inet6.ip6.forwarding=1
+ jexec carp_uni_v6_one ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec carp_uni_v6_one ifconfig ${epair_one}a up
+ jexec carp_uni_v6_one ifconfig ${epair_two}a up
+ jexec carp_uni_v6_one ifconfig ${bridge} inet6 2001:db8::0:4/64 up \
+ no_dad
+ jexec carp_uni_v6_one ifconfig ${bridge} inet6 alias 2001:db8:1::1/64 \
+ no_dad up
+ jexec carp_uni_v6_one ifconfig ${bridge} inet6 alias 2001:db8:2::1/64 \
+ no_dad up
+
+ jexec carp_uni_v6_two ifconfig ${epair_one}b inet6 2001:db8:1::2/64 \
+ no_dad up
+ jexec carp_uni_v6_two route -6 add default 2001:db8:1::1
+ jexec carp_uni_v6_two ifconfig ${epair_one}b inet6 add vhid 1 \
+ peer6 2001:db8:2::2 \
+ 2001:db8::0:1/64
+
+ jexec carp_uni_v6_three ifconfig ${epair_two}b inet6 2001:db8:2::2/64 \
+ no_dad up
+ jexec carp_uni_v6_three route -6 add default 2001:db8:2::1
+ jexec carp_uni_v6_three ifconfig ${epair_two}b inet6 add vhid 1 \
+ peer6 2001:db8:1::2 \
+ 2001:db8::0:1/64
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore jexec carp_uni_v6_two \
+ ping -6 -c 1 2001:db8:2::2
+
+ wait_for_carp carp_uni_v6_two ${epair_one}b \
+ carp_uni_v6_three ${epair_two}b
+
+ atf_check -s exit:0 -o ignore jexec carp_uni_v6_one \
+ ping -6 -c 3 2001:db8::0:1
+}
+
+unicast_v6_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "unicast_ll_v6" "cleanup"
+unicast_ll_v6_head()
+{
+ atf_set descr 'Unicast CARP test (IPv6, link-local)'
+ atf_set require.user root
+}
+
+unicast_ll_v6_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ j=carp_uni_ll_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} 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}_one ifconfig ${bridge} inet6 2001:db8::0:4/64 up \
+ no_dad
+ jexec ${j}_one ifconfig ${bridge} inet6 alias 2001:db8:1::1/64 \
+ no_dad up
+
+ jexec ${j}_two ifconfig ${epair_one}b inet6 2001:db8:1::2/64 \
+ no_dad up
+ jexec ${j}_three ifconfig ${epair_two}b inet6 2001:db8:1::3/64 \
+ no_dad up
+
+ ll_one=$(jexec ${j}_two ifconfig ${epair_one}b | awk "/ .*%${epair_one}b.* / { print \$2 }" | cut -d % -f 1)
+ ll_two=$(jexec ${j}_three ifconfig ${epair_two}b | awk "/ .*%${epair_two}b.* / { print \$2 }" | cut -d % -f 1)
+
+ jexec ${j}_two ifconfig ${epair_one}b inet6 add vhid 1 \
+ peer6 ${ll_two} \
+ 2001:db8::0:1/64
+ jexec ${j}_three ifconfig ${epair_two}b inet6 add vhid 1 \
+ peer6 ${ll_one} \
+ 2001:db8::0:1/64
+
+ # Sanity check
+ atf_check -s exit:0 -o ignore jexec ${j}_two \
+ ping -6 -c 1 2001:db8:1::3
+
+ 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
+}
+
+unicast_ll_v6_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "negative_demotion" "cleanup"
+negative_demotion_head()
+{
+ atf_set descr 'Test PR #259528'
+ atf_set require.user root
+}
+
+negative_demotion_body()
+{
+ carp_init
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail one ${epair}a
+ jexec one sysctl net.inet.carp.preempt=1
+ jexec one ifconfig ${epair}a 192.0.2.1/24 up
+ jexec one ifconfig ${epair}a add vhid 1 192.0.2.254/24 \
+ advskew 0 pass foobar
+
+ vnet_mkjail two ${epair}b
+ jexec two sysctl net.inet.carp.preempt=1
+ jexec two ifconfig ${epair}b 192.0.2.2/24 up
+ jexec two ifconfig ${epair}b add vhid 1 192.0.2.254/24 \
+ advskew 100 pass foobar
+
+ # Allow things to settle
+ wait_for_carp one ${epair}a two ${epair}b
+
+ if is_master one ${epair}a && is_master two ${epair}b
+ then
+ atf_fail "Two masters!"
+ fi
+
+ jexec one sysctl net.inet.carp.demotion=-1
+ sleep 3
+
+ if is_master one ${epair}a && is_master two ${epair}b
+ then
+ atf_fail "Two masters!"
+ fi
+}
+
+negative_demotion_cleanup()
+{
+ vnet_cleanup
+}
+
+
+
+atf_test_case "nd6_ns_source_mac" "cleanup"
+nd6_ns_source_mac_head()
+{
+ atf_set descr 'CARP ndp neighbor solicitation MAC source test (IPv6)'
+ atf_set require.user root
+}
+
+nd6_ns_source_mac_body()
+{
+ carp_init
+ vnet_init_bridge
+
+ bridge=$(vnet_mkbridge)
+ epair_one=$(vnet_mkepair)
+ epair_two=$(vnet_mkepair)
+
+ vnet_mkjail carp_ndp_v6_bridge ${bridge} ${epair_one}a ${epair_two}a
+ vnet_mkjail carp_ndp_v6_master ${epair_one}b
+ vnet_mkjail carp_ndp_v6_slave ${epair_two}b
+
+ jexec carp_ndp_v6_bridge ifconfig ${bridge} inet6 2001:db8::0:4/64 up \
+ no_dad
+ jexec carp_ndp_v6_bridge ifconfig ${bridge} addm ${epair_one}a \
+ addm ${epair_two}a
+ jexec carp_ndp_v6_bridge ifconfig ${epair_one}a up
+ jexec carp_ndp_v6_bridge ifconfig ${epair_two}a up
+
+ jexec carp_ndp_v6_master ifconfig ${epair_one}b inet6 \
+ 2001:db8::1:2/64 up no_dad
+ jexec carp_ndp_v6_master ifconfig ${epair_one}b inet6 add vhid 1 \
+ advskew 0 2001:db8::0:1/64
+
+ jexec carp_ndp_v6_slave ifconfig ${epair_two}b inet6 \
+ 2001:db8::1:3/64 up no_dad
+ jexec carp_ndp_v6_slave ifconfig ${epair_two}b inet6 add vhid 1 \
+ advskew 100 2001:db8::0:1/64
+
+ wait_for_carp carp_ndp_v6_master ${epair_one}b \
+ carp_ndp_v6_slave ${epair_two}b
+
+ # carp_ndp_v6_master is MASTER
+
+ # trigger a NS from the virtual IP from the BACKUP
+ atf_check -s exit:2 -o ignore jexec carp_ndp_v6_slave \
+ ping -6 -c 3 -S 2001:db8::0:1 2001:db8::0:4
+
+ # trigger a NS from the virtual IP from the MASTER,
+ # this ping should work
+ atf_check -s exit:0 -o ignore jexec carp_ndp_v6_master \
+ ping -6 -c 3 -S 2001:db8::0:1 2001:db8::0:4
+
+ # ndp entry should be for the virtual mac
+ atf_check -o match:'2001:db8::1 +00:00:5e:00:01:01' \
+ jexec carp_ndp_v6_bridge ndp -an
+}
+
+nd6_ns_source_mac_cleanup()
+{
+ vnet_cleanup
+}
+
+
+atf_test_case "switch" "cleanup"
+switch_head()
+{
+ atf_set descr 'Switch between master and backup'
+ atf_set require.user root
+}
+
+switch_body()
+{
+ carp_init
+
+ epair=$(vnet_mkepair)
+
+ ifconfig ${epair}a up
+ ifconfig ${epair}a vhid 1 advskew 100 192.0.2.1/24
+ ifconfig ${epair}a vhid 1 state backup
+ ifconfig ${epair}a vhid 1 state master
+}
+
+switch_cleanup()
+{
+ vnet_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"
+ atf_add_test_case "nd6_ns_source_mac"
+ atf_add_test_case "switch"
+}
diff --git a/tests/sys/netinet/divert.sh b/tests/sys/netinet/divert.sh
new file mode 100755
index 000000000000..f521038ba687
--- /dev/null
+++ b/tests/sys/netinet/divert.sh
@@ -0,0 +1,151 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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 "ipdivert_ip_output_remote_success" "cleanup"
+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 python3 scapy
+ atf_set require.kmods ipdivert
+}
+
+ipdivert_ip_output_remote_success_body() {
+
+ if [ "$(atf_config_get ci false)" = "true" ] && \
+ [ "$(uname -p)" = "i386" ]; then
+ atf_skip "https://bugs.freebsd.org/245764"
+ fi
+
+ ids=65530
+ id=`printf "%x" ${ids}`
+ if [ $$ -gt 65535 ]; then
+ xl=`printf "%x" $(($$ - 65535))`
+ yl="1"
+ else
+ xl=`printf "%x" $$`
+ yl=""
+ fi
+
+ vnet_init
+
+ ip4a="192.0.2.5"
+ ip4b="192.0.2.6"
+
+ script_name="../common/divert.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/30
+
+ jname="v4t-${id}-${yl}-${xl}"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/30
+
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --dip ${ip4b} --test_name ipdivert_ip_output_remote_success
+
+ count=`jexec ${jname} netstat -s -p icmp | grep 'Input histogram:' -A8 | grep -c 'echo: '`
+ # Verify redirect got installed
+ atf_check_equal "1" "${count}"
+}
+
+ipdivert_ip_output_remote_success_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_test_case "ipdivert_ip_input_local_success" "cleanup"
+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 python3 scapy
+ atf_set require.kmods ipdivert
+}
+
+ipdivert_ip_input_local_success_body() {
+
+ if [ "$(atf_config_get ci false)" = "true" ] && \
+ [ "$(uname -p)" = "i386" ]; then
+ atf_skip "https://bugs.freebsd.org/245764"
+ fi
+
+ ids=65529
+ id=`printf "%x" ${ids}`
+ if [ $$ -gt 65535 ]; then
+ xl=`printf "%x" $(($$ - 65535))`
+ yl="1"
+ else
+ xl=`printf "%x" $$`
+ yl=""
+ fi
+
+ vnet_init
+
+ ip4a="192.0.2.5"
+ ip4b="192.0.2.6"
+
+ script_name="../common/divert.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/30
+
+ jname="v4t-${id}-${yl}-${xl}"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/30
+
+ atf_check -s exit:0 jexec ${jname} $(atf_get_srcdir)/${script_name} \
+ --sip ${ip4a} --dip ${ip4b} \
+ --test_name ipdivert_ip_input_local_success
+
+ count=`jexec ${jname} netstat -s -p icmp | grep 'Input histogram:' -A8 | grep -c 'echo: '`
+ # Verify redirect got installed
+ atf_check_equal "1" "${count}"
+}
+
+ipdivert_ip_input_local_success_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+
+ atf_add_test_case "ipdivert_ip_output_remote_success"
+ atf_add_test_case "ipdivert_ip_input_local_success"
+}
+
+# end
+
diff --git a/tests/sys/netinet/fibs.sh b/tests/sys/netinet/fibs.sh
new file mode 100755
index 000000000000..bd514b6f8a17
--- /dev/null
+++ b/tests/sys/netinet/fibs.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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 "fibs_ifroutes1_success" "cleanup"
+fibs_ifroutes1_success_head()
+{
+
+ atf_set descr 'Test IPv4 routes gets populated in the correct fib'
+ atf_set require.user root
+}
+
+fibs_ifroutes1_success_body()
+{
+
+ vnet_init
+
+ net_dst="192.168.0."
+ jname="v6t-fibs_ifroutes1_success"
+
+ epair=$(vnet_mkepair)
+ vnet_mkjail ${jname}a ${epair}a
+
+ jexec ${jname}a sysctl net.fibs=2
+
+ jexec ${jname}a ifconfig ${epair}a fib 1
+ jexec ${jname}a ifconfig ${epair}a inet ${net_dst}1/24
+ jexec ${jname}a ifconfig ${epair}a up
+
+ atf_check -s exit:0 -o ignore jexec ${jname}a setfib 1 route -4n get ${net_dst}0/24
+ atf_check -o match:"interface: lo0" jexec ${jname}a setfib 1 route -4n get ${net_dst}1
+ atf_check -o match:"destination: ${net_dst}1" jexec ${jname}a setfib 1 route -4n get ${net_dst}1
+}
+
+fibs_ifroutes1_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "fibs_ifroutes1_success"
+}
+
+
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
new file mode 100644
index 000000000000..2d0b63f8e30a
--- /dev/null
+++ b/tests/sys/netinet/fibs_test.sh
@@ -0,0 +1,830 @@
+#
+# Copyright (c) 2014 Spectra Logic Corporation
+# 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,
+# without modification.
+# 2. Redistributions in binary form must reproduce at minimum a disclaimer
+# substantially similar to the "NO WARRANTY" disclaimer below
+# ("Disclaimer") and any redistribution must be conditioned upon
+# including a substantially similar Disclaimer requirement for further
+# binary redistribution.
+#
+# NO WARRANTY
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES.
+#
+# Authors: Alan Somers (Spectra Logic Corporation)
+#
+
+# 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.
+#
+# Outline:
+# Create two connected epair(4) interfaces
+# Use nping (from security/nmap) to send an ICMP echo request from one
+# interface to the other, spoofing the source IP. The source IP must be
+# spoofed, or else it will already have an entry in the arp table.
+# Check whether an arp entry exists for the spoofed IP
+atf_test_case arpresolve_checks_interface_fib cleanup
+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.progs" "nping"
+}
+arpresolve_checks_interface_fib_body()
+{
+ # Configure the TAP interfaces to use a RFC5737 nonrouteable addresses
+ # and a non-default fib
+ ADDR0="192.0.2.2"
+ ADDR1="192.0.2.3"
+ SUBNET="192.0.2.0"
+ # Due to bug TBD (regressed by multiple_fibs_on_same_subnet) we need
+ # diffferent subnet masks, or FIB1 won't have a subnet route.
+ MASK0="24"
+ MASK1="25"
+ # Spoof a MAC that is reserved per RFC7042
+ SPOOF_ADDR="192.0.2.4"
+ SPOOF_MAC="00:00:5E:00:53:00"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure epair interfaces
+ get_epair
+ setup_iface "$EPAIRA" "$FIB0" inet ${ADDR0} ${MASK0}
+ setup_iface "$EPAIRB" "$FIB1" inet ${ADDR1} ${MASK1}
+
+ # Send an ICMP echo request with a spoofed source IP
+ setfib "$FIB0" nping -c 1 -e ${EPAIRA} -S ${SPOOF_ADDR} \
+ --source-mac ${SPOOF_MAC} --icmp --icmp-type "echo-request" \
+ --icmp-code 0 --icmp-id 0xdead --icmp-seq 1 --data 0xbeef \
+ ${ADDR1}
+ # For informational and debugging purposes only, look for the
+ # characteristic error message
+ dmesg | grep "llinfo.*${SPOOF_ADDR}"
+ # Check that the ARP entry exists
+ atf_check -o match:"${SPOOF_ADDR}.*expires" setfib "$FIB1" arp ${SPOOF_ADDR}
+}
+arpresolve_checks_interface_fib_cleanup()
+{
+ cleanup_ifaces
+}
+
+
+# Regression test for kern/187549
+atf_test_case loopback_and_network_routes_on_nondefault_fib cleanup
+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"
+}
+
+loopback_and_network_routes_on_nondefault_fib_body()
+{
+ # Configure the TAP interface to use an RFC5737 nonrouteable address
+ # and a non-default fib
+ ADDR="192.0.2.2"
+ SUBNET="192.0.2.0"
+ MASK="24"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 1
+
+ # Configure a TAP interface
+ setup_tap ${FIB0} inet ${ADDR} ${MASK}
+
+ # Check whether the host route exists in only the correct FIB
+ setfib ${FIB0} netstat -rn -f inet | grep -q "^${ADDR}.*UHS.*lo0"
+ if [ 0 -ne $? ]; then
+ setfib ${FIB0} netstat -rn -f inet
+ atf_fail "Host route did not appear in the correct FIB"
+ fi
+ setfib 0 netstat -rn -f inet | grep -q "^${ADDR}.*UHS.*lo0"
+ if [ 0 -eq $? ]; then
+ setfib 0 netstat -rn -f inet
+ atf_fail "Host route appeared in the wrong FIB"
+ fi
+
+ # Check whether the network route exists in only the correct FIB
+ setfib ${FIB0} netstat -rn -f inet | \
+ grep -q "^${SUBNET}/${MASK}.*${TAPD}"
+ if [ 0 -ne $? ]; then
+ setfib ${FIB0} netstat -rn -f inet
+ atf_fail "Network route did not appear in the correct FIB"
+ fi
+ setfib 0 netstat -rn -f inet | \
+ grep -q "^${SUBNET}/${MASK}.*${TAPD}"
+ if [ 0 -eq $? ]; then
+ setfib 0 netstat -rn -f inet
+ atf_fail "Network route appeared in the wrong FIB"
+ fi
+}
+
+loopback_and_network_routes_on_nondefault_fib_cleanup()
+{
+ cleanup_ifaces
+}
+
+atf_test_case loopback_and_network_routes_on_nondefault_fib_inet6 cleanup
+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"
+}
+
+loopback_and_network_routes_on_nondefault_fib_inet6_body()
+{
+ # Configure the TAP interface to use a nonrouteable RFC3849
+ # address and a non-default fib
+ ADDR="2001:db8::2"
+ SUBNET="2001:db8::"
+ MASK="64"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 1
+
+ # Configure a TAP interface
+ setup_tap ${FIB0} inet6 ${ADDR} ${MASK}
+
+ # Check whether the host route exists in only the correct FIB
+ setfib ${FIB0} netstat -rn -f inet6 | grep -q "^${ADDR}.*UHS.*lo0"
+ if [ 0 -ne $? ]; then
+ setfib ${FIB0} netstat -rn -f inet6
+ atf_fail "Host route did not appear in the correct FIB"
+ fi
+ setfib 0 netstat -rn -f inet6 | grep -q "^${ADDR}.*UHS.*lo0"
+ if [ 0 -eq $? ]; then
+ setfib 0 netstat -rn -f inet6
+ atf_fail "Host route appeared in the wrong FIB"
+ fi
+
+ # Check whether the network route exists in only the correct FIB
+ setfib ${FIB0} netstat -rn -f inet6 | \
+ grep -q "^${SUBNET}/${MASK}.*${TAPD}"
+ if [ 0 -ne $? ]; then
+ setfib ${FIB0} netstat -rn -f inet6
+ atf_fail "Network route did not appear in the correct FIB"
+ fi
+ setfib 0 netstat -rn -f inet6 | \
+ grep -q "^${SUBNET}/${MASK}.*${TAPD}"
+ if [ 0 -eq $? ]; then
+ setfib 0 netstat -rn -f inet6
+ atf_fail "Network route appeared in the wrong FIB"
+ fi
+}
+
+loopback_and_network_routes_on_nondefault_fib_inet6_cleanup()
+{
+ cleanup_ifaces
+}
+
+
+# Regression test for kern/187552
+atf_test_case default_route_with_multiple_fibs_on_same_subnet cleanup
+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"
+}
+
+default_route_with_multiple_fibs_on_same_subnet_body()
+{
+ # Configure the TAP interfaces to use a RFC5737 nonrouteable addresses
+ # and a non-default fib
+ ADDR0="192.0.2.2"
+ ADDR1="192.0.2.3"
+ GATEWAY="192.0.2.1"
+ SUBNET="192.0.2.0"
+ MASK="24"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure TAP interfaces
+ setup_tap "$FIB0" inet ${ADDR0} ${MASK}
+ TAP0=$TAP
+ setup_tap "$FIB1" inet ${ADDR1} ${MASK}
+ TAP1=$TAP
+
+ # Attempt to add default routes
+ setfib ${FIB0} route add default ${GATEWAY}
+ setfib ${FIB1} route add default ${GATEWAY}
+
+ # Verify that the default route exists for both fibs, with their
+ # respective interfaces.
+ atf_check -o match:"^default.*${TAP0}$" \
+ setfib ${FIB0} netstat -rn -f inet
+ atf_check -o match:"^default.*${TAP1}$" \
+ setfib ${FIB1} netstat -rn -f inet
+}
+
+default_route_with_multiple_fibs_on_same_subnet_cleanup()
+{
+ cleanup_ifaces
+}
+
+atf_test_case default_route_with_multiple_fibs_on_same_subnet_inet6 cleanup
+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"
+}
+
+default_route_with_multiple_fibs_on_same_subnet_inet6_body()
+{
+ # Configure the TAP interfaces to use nonrouteable RFC3849
+ # addresses and non-default FIBs
+ ADDR0="2001:db8::2"
+ ADDR1="2001:db8::3"
+ GATEWAY="2001:db8::1"
+ SUBNET="2001:db8::"
+ MASK="64"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure TAP interfaces
+ setup_tap "$FIB0" inet6 ${ADDR0} ${MASK}
+ TAP0=$TAP
+ setup_tap "$FIB1" inet6 ${ADDR1} ${MASK}
+ TAP1=$TAP
+
+ # Attempt to add default routes
+ setfib ${FIB0} route -6 add default ${GATEWAY}
+ setfib ${FIB1} route -6 add default ${GATEWAY}
+
+ # Verify that the default route exists for both fibs, with their
+ # respective interfaces.
+ atf_check -o match:"^default.*${TAP0}$" \
+ setfib ${FIB0} netstat -rn -f inet6
+ atf_check -o match:"^default.*${TAP1}$" \
+ setfib ${FIB1} netstat -rn -f inet6
+}
+
+default_route_with_multiple_fibs_on_same_subnet_inet6_cleanup()
+{
+ cleanup_ifaces
+}
+
+
+# Regression test for PR kern/189089
+# Create two tap interfaces and assign them both the same IP address but with
+# different netmasks, and both on the default FIB. Then remove one's IP
+# address. Hopefully the machine won't panic.
+atf_test_case same_ip_multiple_ifaces_fib0 cleanup
+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"
+}
+same_ip_multiple_ifaces_fib0_body()
+{
+ 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
+
+ # 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}
+
+ # Do it again, in the opposite order. It should not panic.
+ setup_tap 0 inet ${ADDR} ${MASK0}
+ TAP0=${TAP}
+ setup_tap 0 inet ${ADDR} ${MASK1}
+ TAP1=${TAP}
+ ifconfig ${TAP0} -alias ${ADDR}
+}
+same_ip_multiple_ifaces_fib0_cleanup()
+{
+ cleanup_ifaces
+}
+
+# Regression test for PR kern/189088
+# Test that removing an IP address works even if the same IP is assigned to a
+# different interface, on a different FIB. Tests the same code that whose
+# panic was regressed by same_ip_multiple_ifaces_fib0.
+# Create two tap interfaces and assign them both the same IP address but with
+# different netmasks, and on different FIBs. Then remove one's IP
+# address. Hopefully the machine won't panic. Also, the IP's hostroute should
+# dissappear from the correct fib.
+atf_test_case same_ip_multiple_ifaces cleanup
+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"
+}
+same_ip_multiple_ifaces_body()
+{
+ 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 4
+
+ # Setup the interfaces, then remove one alias. It should not panic.
+ setup_tap ${FIB0} inet ${ADDR} ${MASK0}
+ TAP0=${TAP}
+ setup_tap ${FIB1} inet ${ADDR} ${MASK1}
+ TAP1=${TAP}
+ ifconfig ${TAP1} -alias ${ADDR}
+ atf_check -o not-match:"^${ADDR}[[:space:]]" \
+ setfib ${FIB1} netstat -rn -f inet
+
+ # Do it again, in the opposite order. It should not panic.
+ setup_tap ${FIB2} inet ${ADDR} ${MASK0}
+ TAP0=${TAP}
+ setup_tap ${FIB3} inet ${ADDR} ${MASK1}
+ TAP1=${TAP}
+ ifconfig ${TAP0} -alias ${ADDR}
+ atf_check -o not-match:"^${ADDR}[[:space:]]" \
+ setfib ${FIB2} netstat -rn -f inet
+}
+same_ip_multiple_ifaces_cleanup()
+{
+ # Due to PR kern/189088, we must destroy the interfaces in LIFO order
+ # in order for the routes to be correctly cleaned up.
+ for TAPD in `tail -r "ifaces_to_cleanup"`; do
+ echo ifconfig ${TAPD} destroy
+ ifconfig ${TAPD} destroy
+ done
+}
+
+atf_test_case same_ip_multiple_ifaces_inet6 cleanup
+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"
+}
+same_ip_multiple_ifaces_inet6_body()
+{
+ ADDR="2001:db8::2"
+ MASK0="64"
+ MASK1="128"
+
+ # Unlike most of the tests in this file, this is applicable regardless
+ # of net.add_addr_allfibs
+ get_fibs 2
+
+ # Setup the interfaces, then remove one alias. It should not panic.
+ setup_tap ${FIB0} inet6 ${ADDR} ${MASK0}
+ TAP0=${TAP}
+ setup_tap ${FIB1} inet6 ${ADDR} ${MASK1}
+ TAP1=${TAP}
+ atf_check -s exit:0 ifconfig ${TAP1} inet6 ${ADDR} -alias
+ atf_check -o not-match:"^${ADDR}[[:space:]]" \
+ setfib ${FIB1} netstat -rn -f inet6
+ ifconfig ${TAP1} destroy
+ ifconfig ${TAP0} destroy
+
+ # Do it again, in the opposite order. It should not panic.
+ setup_tap ${FIB0} inet6 ${ADDR} ${MASK0}
+ TAP0=${TAP}
+ setup_tap ${FIB1} inet6 ${ADDR} ${MASK1}
+ TAP1=${TAP}
+ atf_check -s exit:0 ifconfig ${TAP0} inet6 ${ADDR} -alias
+ atf_check -o not-match:"^${ADDR}[[:space:]]" \
+ setfib ${FIB0} netstat -rn -f inet6
+}
+same_ip_multiple_ifaces_inet6_cleanup()
+{
+ cleanup_ifaces
+}
+
+atf_test_case slaac_on_nondefault_fib6 cleanup
+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" "allow_sysctl_side_effects"
+}
+slaac_on_nondefault_fib6_body()
+{
+ # Configure the epair interfaces to use nonrouteable RFC3849
+ # addresses and non-default FIBs
+ PREFIX="2001:db8:$(printf "%x" `jot -r 1 0 65535`):$(printf "%x" `jot -r 1 0 65535`)"
+ ADDR="$PREFIX::2"
+ GATEWAY="$PREFIX::1"
+ SUBNET="$PREFIX:"
+ MASK="64"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ sysctl -n "net.inet6.ip6.rfc6204w3" >> "rfc6204w3.state"
+ sysctl -n "net.inet6.ip6.forwarding" >> "forwarding.state"
+ # Enable forwarding so the kernel will send RAs
+ sysctl net.inet6.ip6.forwarding=1
+ # Enable RFC6204W3 mode so the kernel will enable default router
+ # selection while also forwarding packets
+ sysctl net.inet6.ip6.rfc6204w3=1
+
+ # Configure epair interfaces
+ get_epair
+ setup_iface "$EPAIRA" "$FIB0" inet6 ${ADDR} ${MASK}
+ echo setfib $FIB1 ifconfig "$EPAIRB" inet6 -ifdisabled accept_rtadv fib $FIB1 up
+ setfib $FIB1 ifconfig "$EPAIRB" inet6 -ifdisabled accept_rtadv fib $FIB1 up
+ rtadvd -p rtadvd.pid -C rtadvd.sock -c /dev/null "$EPAIRA"
+ rtsol "$EPAIRB"
+
+ # Check SLAAC address
+ atf_check -o match:"inet6 ${SUBNET}.*prefixlen ${MASK}.*autoconf" \
+ ifconfig "$EPAIRB"
+ # Check local route
+ atf_check -o match:"${SUBNET}.*\<UHS\>.*lo0" \
+ netstat -rnf inet6 -F $FIB1
+ # Check subnet route
+ atf_check -o match:"${SUBNET}:/${MASK}.*\<U\>.*$EPAIRB" \
+ netstat -rnf inet6 -F $FIB1
+ # Check default route
+ atf_check -o match:"default.*\<UG\>.*$EPAIRB" \
+ netstat -rnf inet6 -F $FIB1
+
+ # Check that none of the above routes appeared on other routes
+ for fib in $( seq 0 $(($(sysctl -n net.fibs) - 1))); do
+ if [ "$fib" = "$FIB1" -o "$fib" = "$FIB0" ]; then
+ continue
+ fi
+ atf_check -o not-match:"${SUBNET}.*\<UHS\>.*lo0" \
+ netstat -rnf inet6 -F $fib
+ atf_check -o not-match:"${SUBNET}:/${MASK}.*\<U\>.*$EPAIRB" \
+ netstat -rnf inet6 -F $fib
+ atf_check -o not-match:"default.*\<UG\>.*$EPAIRB" \
+ netstat -rnf inet6 -F $fib
+ done
+}
+slaac_on_nondefault_fib6_cleanup()
+{
+ if [ -f "rtadvd.pid" ]; then
+ # rtadvd can take a long time to shutdown. Use SIGKILL to kill
+ # it right away. The downside to using SIGKILL is that it
+ # won't send final RAs to all interfaces, but we don't care
+ # because we're about to destroy its interface anyway.
+ pkill -kill -F rtadvd.pid
+ rm -f rtadvd.pid
+ fi
+ cleanup_ifaces
+ if [ -f "forwarding.state" ] ; then
+ sysctl "net.inet6.ip6.forwarding"=`cat "forwarding.state"`
+ rm "forwarding.state"
+ fi
+ if [ -f "rfc6204w3.state" ] ; then
+ sysctl "net.inet6.ip6.rfc6204w3"=`cat "rfc6204w3.state"`
+ rm "rfc6204w3.state"
+ fi
+}
+
+# Regression test for kern/187550
+atf_test_case subnet_route_with_multiple_fibs_on_same_subnet cleanup
+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"
+}
+
+subnet_route_with_multiple_fibs_on_same_subnet_body()
+{
+ # Configure the TAP interfaces to use a RFC5737 nonrouteable addresses
+ # and a non-default fib
+ ADDR0="192.0.2.2"
+ ADDR1="192.0.2.3"
+ SUBNET="192.0.2.0"
+ MASK="24"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure TAP interfaces
+ setup_tap "$FIB0" inet ${ADDR0} ${MASK}
+ setup_tap "$FIB1" inet ${ADDR1} ${MASK}
+
+ # Check that a subnet route exists on both fibs
+ atf_check -o ignore setfib "$FIB0" route get $ADDR1
+ atf_check -o ignore setfib "$FIB1" route get $ADDR0
+}
+
+subnet_route_with_multiple_fibs_on_same_subnet_cleanup()
+{
+ cleanup_ifaces
+}
+
+atf_test_case subnet_route_with_multiple_fibs_on_same_subnet_inet6 cleanup
+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"
+}
+
+subnet_route_with_multiple_fibs_on_same_subnet_inet6_body()
+{
+ # Configure the TAP interfaces to use a RFC3849 nonrouteable addresses
+ # and a non-default fib
+ ADDR0="2001:db8::2"
+ ADDR1="2001:db8::3"
+ SUBNET="2001:db8::"
+ MASK="64"
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure TAP interfaces
+ setup_tap "$FIB0" inet6 ${ADDR0} ${MASK}
+ setup_tap "$FIB1" inet6 ${ADDR1} ${MASK}
+
+ # Check that a subnet route exists on both fibs
+ atf_check -o ignore setfib "$FIB0" route -6 get $ADDR1
+ atf_check -o ignore setfib "$FIB1" route -6 get $ADDR0
+}
+
+subnet_route_with_multiple_fibs_on_same_subnet_inet6_cleanup()
+{
+ cleanup_ifaces
+}
+
+# Test that source address selection works correctly for UDP packets with
+# SO_DONTROUTE set that are sent on non-default FIBs.
+# This bug was discovered with "setfib 1 netperf -t UDP_STREAM -H some_host"
+# Regression test for kern/187553
+#
+# The root cause was that ifa_ifwithnet() did not have a fib argument. It
+# would return an address from an interface on any FIB that had a subnet route
+# for the destination. If more than one were available, it would choose the
+# most specific. This is most easily tested by creating a FIB without a
+# default route, then trying to send a UDP packet with SO_DONTROUTE set to an
+# address which is not routable on that FIB. Absent the fix for this bug,
+# in_pcbladdr would choose an interface on any FIB with a default route. With
+# the fix, you will get EUNREACH or ENETUNREACH.
+atf_test_case udp_dontroute cleanup
+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"
+}
+
+udp_dontroute_body()
+{
+ # Configure the TAP interface to use an RFC5737 nonrouteable address
+ # and a non-default fib
+ ADDR0="192.0.2.2"
+ ADDR1="192.0.2.3"
+ SUBNET="192.0.2.0"
+ MASK="24"
+ # Use a different IP on the same subnet as the target
+ TARGET="192.0.2.100"
+ SRCDIR=`atf_get_srcdir`
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure the TAP interfaces
+ setup_tap ${FIB0} inet ${ADDR0} ${MASK}
+ TARGET_TAP=${TAP}
+ setup_tap ${FIB1} inet ${ADDR1} ${MASK}
+
+ # Send a UDP packet with SO_DONTROUTE. In the failure case, it will
+ # return ENETUNREACH, or send the packet to the wrong tap
+ atf_check -o ignore setfib ${FIB0} \
+ ${SRCDIR}/udp_dontroute ${TARGET} /dev/${TARGET_TAP}
+ cleanup_ifaces
+
+ # Repeat, but this time target the other tap
+ setup_tap ${FIB0} inet ${ADDR0} ${MASK}
+ setup_tap ${FIB1} inet ${ADDR1} ${MASK}
+ TARGET_TAP=${TAP}
+
+ atf_check -o ignore setfib ${FIB1} \
+ ${SRCDIR}/udp_dontroute ${TARGET} /dev/${TARGET_TAP}
+}
+
+udp_dontroute_cleanup()
+{
+ cleanup_ifaces
+}
+
+atf_test_case udp_dontroute6 cleanup
+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"
+}
+
+udp_dontroute6_body()
+{
+ if [ "$(atf_config_get ci false)" = "true" ]; then
+ atf_skip "https://bugs.freebsd.org/244172"
+ fi
+ # Configure the TAP interface to use an RFC3849 nonrouteable address
+ # and a non-default fib
+ ADDR0="2001:db8::2"
+ ADDR1="2001:db8::3"
+ SUBNET="2001:db8::"
+ MASK="64"
+ # Use a different IP on the same subnet as the target
+ TARGET="2001:db8::100"
+ SRCDIR=`atf_get_srcdir`
+
+ # Check system configuration
+ if [ 0 != `sysctl -n net.add_addr_allfibs` ]; then
+ atf_skip "This test requires net.add_addr_allfibs=0"
+ fi
+ get_fibs 2
+
+ # Configure the TAP interfaces. Use no_dad so the addresses will be
+ # ready right away and won't be marked as tentative until DAD
+ # completes.
+ setup_tap ${FIB0} inet6 ${ADDR0} ${MASK} no_dad
+ TARGET_TAP=${TAP}
+ setup_tap ${FIB1} inet6 ${ADDR1} ${MASK} no_dad
+
+ # Send a UDP packet with SO_DONTROUTE. In the failure case, it will
+ # return ENETUNREACH, or send the packet to the wrong tap
+ atf_check -o ignore setfib ${FIB0} \
+ ${SRCDIR}/udp_dontroute -6 ${TARGET} /dev/${TARGET_TAP}
+ cleanup_ifaces
+
+ # Repeat, but this time target the other tap
+ setup_tap ${FIB0} inet6 ${ADDR0} ${MASK} no_dad
+ setup_tap ${FIB1} inet6 ${ADDR1} ${MASK} no_dad
+ TARGET_TAP=${TAP}
+
+ atf_check -o ignore setfib ${FIB1} \
+ ${SRCDIR}/udp_dontroute -6 ${TARGET} /dev/${TARGET_TAP}
+}
+
+udp_dontroute6_cleanup()
+{
+ cleanup_ifaces
+}
+
+
+atf_init_test_cases()
+{
+ atf_add_test_case arpresolve_checks_interface_fib
+ atf_add_test_case loopback_and_network_routes_on_nondefault_fib
+ atf_add_test_case loopback_and_network_routes_on_nondefault_fib_inet6
+ atf_add_test_case default_route_with_multiple_fibs_on_same_subnet
+ atf_add_test_case default_route_with_multiple_fibs_on_same_subnet_inet6
+ atf_add_test_case same_ip_multiple_ifaces_fib0
+ atf_add_test_case same_ip_multiple_ifaces
+ atf_add_test_case same_ip_multiple_ifaces_inet6
+ atf_add_test_case slaac_on_nondefault_fib6
+ atf_add_test_case subnet_route_with_multiple_fibs_on_same_subnet
+ atf_add_test_case subnet_route_with_multiple_fibs_on_same_subnet_inet6
+ atf_add_test_case udp_dontroute
+ atf_add_test_case udp_dontroute6
+}
+
+# Looks up one or more fibs from the configuration data and validates them.
+# Returns the results in the env varilables FIB0, FIB1, etc.
+
+# parameter numfibs The number of fibs to lookup
+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
+ eval FIB${i}=$(($i + 1))
+ i=$(( $i + 1 ))
+ done
+}
+
+# Creates a new pair of connected epair(4) interface, registers them for
+# cleanup, and returns their namen via the environment variables EPAIRA and
+# EPAIRB
+get_epair()
+{
+ local EPAIRD
+
+ if (which pfctl && pfctl -s info | grep -q 'Status: Enabled') ||
+ [ `sysctl -n net.inet.ip.fw.enable` = "1" ] ||
+ (which ipf && ipf -V); then
+ atf_skip "firewalls interfere with this test"
+ fi
+
+ if EPAIRD=`ifconfig epair create`; then
+ # Record the epair device so we can clean it up later
+ echo ${EPAIRD} >> "ifaces_to_cleanup"
+ EPAIRA=${EPAIRD}
+ EPAIRB=${EPAIRD%a}b
+ else
+ atf_skip "Could not create epair(4) interfaces"
+ fi
+}
+
+# Creates a new tap(4) interface, registers it for cleanup, and returns the
+# name via the environment variable TAP
+get_tap()
+{
+ local TAPD
+
+ if TAPD=`ifconfig tap create`; then
+ # Record the TAP device so we can clean it up later
+ echo ${TAPD} >> "ifaces_to_cleanup"
+ TAP=${TAPD}
+ else
+ atf_skip "Could not create a tap(4) interface"
+ fi
+}
+
+# Configure an ethernet interface
+# parameters:
+# Interface name
+# fib
+# Protocol (inet or inet6)
+# IP address
+# Netmask in number of bits (eg 24 or 8)
+# Extra flags
+# Return: None
+setup_iface()
+{
+ local IFACE=$1
+ local FIB=$2
+ local PROTO=$3
+ local ADDR=$4
+ local MASK=$5
+ local FLAGS=$6
+ atf_check setfib ${FIB} ifconfig $IFACE ${PROTO} ${ADDR}/${MASK} fib $FIB $FLAGS
+}
+
+# Create a tap(4) interface, configure it, and register it for cleanup.
+# parameters:
+# fib
+# Protocol (inet or inet6)
+# IP address
+# Netmask in number of bits (eg 24 or 8)
+# Extra flags
+# Return: the tap interface name as the env variable TAP
+setup_tap()
+{
+ get_tap
+ setup_iface "$TAP" "$@"
+}
+
+cleanup_ifaces()
+{
+ if [ -f ifaces_to_cleanup ]; then
+ for iface in $(cat ifaces_to_cleanup); do
+ echo ifconfig "${iface}" destroy
+ ifconfig "${iface}" destroy 2>/dev/null || true
+ done
+ rm -f ifaces_to_cleanup
+ fi
+}
diff --git a/tests/sys/netinet/forward.sh b/tests/sys/netinet/forward.sh
new file mode 100755
index 000000000000..be69e91b6137
--- /dev/null
+++ b/tests/sys/netinet/forward.sh
@@ -0,0 +1,325 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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 "fwd_ip_icmp_iface_fast_success" "cleanup"
+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 python3 scapy
+}
+
+fwd_ip_icmp_iface_fast_success_body() {
+
+ vnet_init
+
+ ip4a="192.0.2.1"
+ ip4b="192.0.2.2"
+ plen=29
+ src_ip="192.0.2.3"
+
+ script_name="../common/sender.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/${plen}
+
+ jname="v4t-fwd_ip_icmp_iface_fast_success"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/${plen}
+
+ # Get router ip/mac
+ jail_ip=${ip4b}
+ jail_mac=`jexec ${jname} ifconfig ${epair}b ether | awk '$1~/ether/{print$2}'`
+
+ our_mac=`ifconfig ${epair}a ether | awk '$1~/ether/{print$2}'`
+
+ jexec ${jname} sysctl net.inet.ip.forwarding=1
+ # As we're doing router-on-the-stick, turn sending IP redirects off:
+ jexec ${jname} sysctl net.inet.ip.redirect=0
+
+ # echo "LOCAL: ${local_ip} ${local_mac}"
+ # echo "REMOTE: ${remote_rtr_ip} ${remote_rtr_mac}"
+
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --test_name fwd_ip_icmp_fast \
+ --smac ${our_mac} --dmac ${jail_mac} \
+ --sip ${src_ip} --dip ${ip4a} \
+ --iface ${epair}a
+
+ # check counters are valid
+ atf_check -o match:'1 packet forwarded \(1 packet fast forwarded\)' jexec ${jname} netstat -sp ip
+}
+
+fwd_ip_icmp_iface_fast_success_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_test_case "fwd_ip_icmp_gw_fast_success" "cleanup"
+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 python3 scapy
+}
+
+fwd_ip_icmp_gw_fast_success_body() {
+
+ vnet_init
+
+ ip4a="192.0.2.1"
+ ip4b="192.0.2.2"
+ plen=29
+ src_ip="192.0.2.3"
+ dst_ip="192.0.2.4"
+
+ script_name="../common/sender.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/${plen}
+
+ jname="v4t-fwd_ip_icmp_gw_fast_success"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/${plen}
+
+ # Get router ip/mac
+ jail_ip=${ip4b}
+ jail_mac=`jexec ${jname} ifconfig ${epair}b ether | awk '$1~/ether/{print$2}'`
+
+ our_mac=`ifconfig ${epair}a ether | awk '$1~/ether/{print$2}'`
+
+ jexec ${jname} sysctl net.inet.ip.forwarding=1
+ # As we're doing router-on-the-stick, turn sending IP redirects off:
+ jexec ${jname} sysctl net.inet.ip.redirect=0
+
+ # Add host route
+ jexec ${jname} route -4 add -host ${dst_ip} ${ip4a}
+
+ # echo "LOCAL: ${local_ip} ${local_mac}"
+ # echo "REMOTE: ${remote_rtr_ip} ${remote_rtr_mac}"
+
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --test_name fwd_ip_icmp_fast \
+ --smac ${our_mac} --dmac ${jail_mac} \
+ --sip ${src_ip} --dip ${dst_ip} \
+ --iface ${epair}a
+
+ # check counters are valid
+ atf_check -o match:'1 packet forwarded \(1 packet fast forwarded\)' jexec ${jname} netstat -sp ip
+}
+
+fwd_ip_icmp_gw_fast_success_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_test_case "fwd_ip_icmp_iface_slow_success" "cleanup"
+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 python3 scapy
+}
+
+fwd_ip_icmp_iface_slow_success_body() {
+
+ vnet_init
+
+ ip4a="192.0.2.1"
+ ip4b="192.0.2.2"
+ plen=29
+ src_ip="192.0.2.3"
+
+ script_name="../common/sender.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/${plen}
+
+ jname="v4t-fwd_ip_icmp_iface_slow_success"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/${plen}
+
+ # Get router ip/mac
+ jail_ip=${ip4b}
+ jail_mac=`jexec ${jname} ifconfig ${epair}b ether | awk '$1~/ether/{print$2}'`
+
+ our_mac=`ifconfig ${epair}a ether | awk '$1~/ether/{print$2}'`
+
+ jexec ${jname} sysctl net.inet.ip.forwarding=1
+ # As we're doing router-on-the-stick, turn sending IP redirects off:
+ jexec ${jname} sysctl net.inet.ip.redirect=0
+
+ # Generate packet with options to force slow-path
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --test_name fwd_ip_icmp_slow \
+ --smac ${our_mac} --dmac ${jail_mac} \
+ --sip ${src_ip} --dip ${ip4a} \
+ --iface ${epair}a
+
+ # check counters are valid
+ atf_check -o match:'1 packet forwarded \(0 packets fast forwarded\)' jexec ${jname} netstat -sp ip
+}
+
+fwd_ip_icmp_iface_slow_success_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_test_case "fwd_ip_icmp_gw_slow_success" "cleanup"
+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 python3 scapy
+}
+
+fwd_ip_icmp_gw_slow_success_body() {
+
+ vnet_init
+
+ ip4a="192.0.2.1"
+ ip4b="192.0.2.2"
+ plen=29
+ src_ip="192.0.2.3"
+ dst_ip="192.0.2.4"
+
+ script_name="../common/sender.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/${plen}
+
+ jname="v4t-fwd_ip_icmp_gw_slow_success"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/${plen}
+
+ # Get router ip/mac
+ jail_ip=${ip4b}
+ jail_mac=`jexec ${jname} ifconfig ${epair}b ether | awk '$1~/ether/{print$2}'`
+
+ our_mac=`ifconfig ${epair}a ether | awk '$1~/ether/{print$2}'`
+
+ jexec ${jname} sysctl net.inet.ip.forwarding=1
+ # As we're doing router-on-the-stick, turn sending IP redirects off:
+ jexec ${jname} sysctl net.inet.ip.redirect=0
+
+ # Add host route
+ jexec ${jname} route -4 add -host ${dst_ip} ${ip4a}
+
+ # echo "LOCAL: ${local_ip} ${local_mac}"
+ # echo "REMOTE: ${remote_rtr_ip} ${remote_rtr_mac}"
+
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --test_name fwd_ip_icmp_fast \
+ --smac ${our_mac} --dmac ${jail_mac} \
+ --sip ${src_ip} --dip ${dst_ip} \
+ --iface ${epair}a
+
+ # check counters are valid
+ atf_check -o match:'1 packet forwarded \(1 packet fast forwarded\)' jexec ${jname} netstat -sp ip
+}
+
+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()
+{
+
+ atf_add_test_case "fwd_ip_icmp_iface_fast_success"
+ 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
new file mode 100644
index 000000000000..feb9b8b571d5
--- /dev/null
+++ b/tests/sys/netinet/igmp.py
@@ -0,0 +1,157 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2023 Rubicon Communications, LLC (Netgate)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+import pytest
+from atf_python.sys.net.tools import ToolsHelper
+from atf_python.sys.net.vnet import VnetTestTemplate
+import os
+import socket
+import struct
+import sys
+import logging
+logging.getLogger("scapy").setLevel(logging.CRITICAL)
+curdir = os.path.dirname(os.path.realpath(__file__))
+netpfil_common = curdir + "/../netpfil/common"
+sys.path.append(netpfil_common)
+
+sc = None
+sp = None
+
+def check_igmpv3(args, pkt):
+ igmp = pkt.getlayer(sc.igmpv3.IGMPv3)
+ if igmp is None:
+ return False
+
+ igmpmr = pkt.getlayer(sc.igmpv3.IGMPv3mr)
+ if igmpmr is None:
+ return False
+
+ for r in igmpmr.records:
+ if r.maddr != args["group"]:
+ return False
+ if args["type"] == "join":
+ if r.rtype != 4:
+ return False
+ elif args["type"] == "leave":
+ if r.rtype != 3:
+ return False
+ r.show()
+
+ 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 = {
+ "vnet1": { "ifaces": [ "if1" ] },
+ "if1": { "prefixes4": [ ("192.0.2.1/24", "192.0.2.2/24" ) ] },
+ }
+
+ def setup_method(self, method):
+ global sc
+ if sc is None:
+ import scapy.contrib as _sc
+ import scapy.contrib.igmp
+ import scapy.contrib.igmpv3
+ import scapy.all as _sp
+ sc = _sc
+ 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 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)
+
+ # 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_igmpv3, if1.name)
+
+ 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/ip6_v4mapped_test.c b/tests/sys/netinet/ip6_v4mapped_test.c
new file mode 100644
index 000000000000..fa23192f56e4
--- /dev/null
+++ b/tests/sys/netinet/ip6_v4mapped_test.c
@@ -0,0 +1,398 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2022 Michael J. Karels.
+ * Copyright (c) 2020 Netflix, Inc.
+ *
+ * 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.
+ */
+
+/*
+ * This test is derived from tcp_connect_port_test.c.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/sysctl.h>
+
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <netdb.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define SYSCTLBAKFILE "tmp.net.inet.ip.portrange.values"
+
+#define PORT_FIRST 10000 /* normal default */
+#define PORT_LAST 10003
+#define LOOPS 10 /* 5 should be enough */
+
+struct portrange {
+ int first;
+ int last;
+};
+
+/*
+ * Set first and last ports in the ipport range. Save the old values
+ * of the sysctls so they can be restored later.
+ */
+static void
+set_portrange(void)
+{
+ int error, fd, first_new, last_new;
+ struct portrange save_ports;
+ size_t sysctlsz;
+
+ /*
+ * Pre-emptively unlink our restoration file, so we will do no
+ * restoration on error.
+ */
+ unlink(SYSCTLBAKFILE);
+
+ /*
+ * Set the net.inet.ip.portrange.{first,last} sysctls. Save the
+ * old values so we can restore them.
+ */
+ first_new = PORT_FIRST;
+ sysctlsz = sizeof(save_ports.first);
+ error = sysctlbyname("net.inet.ip.portrange.first", &save_ports.first,
+ &sysctlsz, &first_new, sizeof(first_new));
+ if (error) {
+ warn("sysctlbyname(\"net.inet.ip.portrange.first\") "
+ "failed");
+ atf_tc_skip("Unable to set sysctl");
+ }
+ if (sysctlsz != sizeof(save_ports.first)) {
+ fprintf(stderr, "Error: unexpected sysctl value size "
+ "(expected %zu, actual %zu)\n", sizeof(save_ports.first),
+ sysctlsz);
+ goto restore_sysctl;
+ }
+
+ last_new = PORT_LAST;
+ sysctlsz = sizeof(save_ports.last);
+ error = sysctlbyname("net.inet.ip.portrange.last", &save_ports.last,
+ &sysctlsz, &last_new, sizeof(last_new));
+ if (error) {
+ warn("sysctlbyname(\"net.inet.ip.portrange.last\") "
+ "failed");
+ atf_tc_skip("Unable to set sysctl");
+ }
+ if (sysctlsz != sizeof(save_ports.last)) {
+ fprintf(stderr, "Error: unexpected sysctl value size "
+ "(expected %zu, actual %zu)\n", sizeof(save_ports.last),
+ sysctlsz);
+ goto restore_sysctl;
+ }
+
+ /* Open the backup file, write the contents, and close it. */
+ fd = open(SYSCTLBAKFILE, O_WRONLY|O_CREAT|O_TRUNC|O_EXCL,
+ S_IRUSR|S_IWUSR);
+ if (fd < 0) {
+ warn("error opening sysctl backup file");
+ goto restore_sysctl;
+ }
+ error = write(fd, &save_ports, sizeof(save_ports));
+ if (error < 0) {
+ warn("error writing saved value to sysctl backup file");
+ goto cleanup_and_restore;
+ }
+ if (error != (int)sizeof(save_ports)) {
+ fprintf(stderr,
+ "Error writing saved value to sysctl backup file: "
+ "(expected %zu, actual %d)\n", sizeof(save_ports), error);
+ goto cleanup_and_restore;
+ }
+ error = close(fd);
+ if (error) {
+ warn("error closing sysctl backup file");
+cleanup_and_restore:
+ (void)close(fd);
+ (void)unlink(SYSCTLBAKFILE);
+restore_sysctl:
+ sysctlsz = sizeof(save_ports.first);
+ (void)sysctlbyname("net.inet.ip.portrange.first", NULL,
+ NULL, &save_ports.first, sysctlsz);
+ sysctlsz = sizeof(save_ports.last);
+ (void)sysctlbyname("net.inet.ip.portrange.last", NULL,
+ NULL, &save_ports.last, sysctlsz);
+ atf_tc_skip("Error setting sysctl");
+ }
+}
+
+/*
+ * Restore the sysctl values from the backup file and delete the backup file.
+ */
+static void
+restore_portrange(void)
+{
+ int error, fd;
+ struct portrange save_ports;
+
+ /* Open the backup file, read the contents, close it, and delete it. */
+ fd = open(SYSCTLBAKFILE, O_RDONLY);
+ if (fd < 0) {
+ warn("error opening sysctl backup file");
+ return;
+ }
+ error = read(fd, &save_ports, sizeof(save_ports));
+ if (error < 0) {
+ warn("error reading saved values from sysctl backup file");
+ return;
+ }
+ if (error != (int)sizeof(save_ports)) {
+ fprintf(stderr,
+ "Error reading saved values from sysctl backup file: "
+ "(expected %zu, actual %d)\n", sizeof(save_ports), error);
+ return;
+ }
+ error = close(fd);
+ if (error)
+ warn("error closing sysctl backup file");
+ error = unlink(SYSCTLBAKFILE);
+ if (error)
+ warn("error removing sysctl backup file");
+
+ /* Restore the saved sysctl values. */
+ error = sysctlbyname("net.inet.ip.portrange.first", NULL, NULL,
+ &save_ports.first, sizeof(save_ports.first));
+ if (error)
+ warn("sysctlbyname(\"net.inet.ip.portrange.first\") "
+ "failed while restoring value");
+ error = sysctlbyname("net.inet.ip.portrange.last", NULL, NULL,
+ &save_ports.last, sizeof(save_ports.last));
+ if (error)
+ warn("sysctlbyname(\"net.inet.ip.portrange.last\") "
+ "failed while restoring value");
+}
+
+ATF_TC_WITH_CLEANUP(tcp_v4mapped_bind);
+ATF_TC_HEAD(tcp_v4mapped_bind, tc)
+{
+ /* root is only required for sysctls (setup and cleanup). */
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "require.config", "allow_sysctl_side_effects");
+ atf_tc_set_md_var(tc, "descr",
+ "Check local port assignment with bind and mapped V4 addresses");
+}
+/*
+ * Create a listening IPv4 socket, then connect to it repeatedly using a
+ * bound IPv6 socket using a v4 mapped address. With a small port range,
+ * this should fail on a bind() call with EADDRNOTAVAIL. However, in
+ * previous systems, the bind() would succeed, binding a duplicate port,
+ * and then the connect would fail with EADDRINUSE. Make sure we get
+ * the right error.
+ */
+ATF_TC_BODY(tcp_v4mapped_bind, tc)
+{
+ union {
+ struct sockaddr saddr;
+ struct sockaddr_in saddr4;
+ struct sockaddr_in6 saddr6;
+ } su_clnt, su_srvr, su_mapped;
+ struct addrinfo ai_hint, *aip;
+ socklen_t salen;
+ int csock, error, i, lsock, off = 0;
+ bool got_bind_error = false;
+
+ /*
+ * Set the net.inet.ip.portrange.{first,last} sysctls to use a small
+ * range, allowing us to generate port exhaustion quickly.
+ */
+ set_portrange();
+
+ /* Setup the listen socket. */
+ lsock = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(lsock >= 0, "socket() for listen socket failed: %s",
+ strerror(errno));
+
+ memset(&su_srvr.saddr4, 0, sizeof(su_srvr.saddr4));
+ su_srvr.saddr4.sin_family = AF_INET;
+ error = bind(lsock, &su_srvr.saddr, sizeof(su_srvr.saddr4));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+ error = listen(lsock, LOOPS + 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ /* Get the address of the listen socket. */
+ salen = sizeof(su_srvr);
+ error = getsockname(lsock, &su_srvr.saddr, &salen);
+ ATF_REQUIRE_MSG(error == 0,
+ "getsockname() for listen socket failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(salen == sizeof(struct sockaddr_in),
+ "unexpected sockaddr size");
+ ATF_REQUIRE_MSG(su_srvr.saddr.sa_len == sizeof(struct sockaddr_in),
+ "unexpected sa_len size");
+
+ /* Set up destination address for client sockets. */
+ memset(&ai_hint, 0, sizeof(ai_hint));
+ ai_hint.ai_family = AF_INET6;
+ ai_hint.ai_flags = AI_NUMERICHOST | AI_V4MAPPED;
+ error = getaddrinfo("127.0.0.1", NULL, &ai_hint, &aip);
+ ATF_REQUIRE_MSG(error == 0, "getaddrinfo: %s", gai_strerror(error));
+ memcpy(&su_mapped.saddr6, aip->ai_addr, sizeof(su_mapped.saddr6));
+ su_mapped.saddr6.sin6_port = su_srvr.saddr4.sin_port;
+ freeaddrinfo(aip);
+
+ /* Set up address to bind for client sockets (unspecified). */
+ memset(&su_clnt.saddr6, 0, sizeof(su_clnt.saddr6));
+ su_clnt.saddr6.sin6_family = AF_INET6;
+
+ /* Open connections in a loop. */
+ for (i = 0; i < LOOPS; i++) {
+ csock = socket(PF_INET6, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(csock >= 0,
+ "socket() for client socket %d failed: %s",
+ i, strerror(errno));
+ error = setsockopt(csock, IPPROTO_IPV6, IPV6_V6ONLY, &off,
+ sizeof(off));
+ ATF_REQUIRE_MSG(error == 0,
+ "setsockopt(IPV6_ONLY = 0) failed: %s", strerror(errno));
+
+ /*
+ * A bind would not be necessary for operation, but
+ * provokes the error.
+ */
+ error = bind(csock, &su_clnt.saddr, sizeof(su_clnt.saddr6));
+ if (error != 0) {
+ if (errno == EADDRNOTAVAIL) { /* Success, expected */
+ got_bind_error = true;
+ break;
+ }
+ ATF_REQUIRE_MSG(error == 0,
+ "client bind %d failed: %s", i, strerror(errno));
+ }
+
+ error = connect(csock, &su_mapped.saddr, su_mapped.saddr.sa_len);
+ if (error != 0 && errno == EADDRINUSE) {
+ /* This is the specific error we were looking for. */
+ atf_tc_fail("client connect %d failed, "
+ " client had duplicate port: %s",
+ i, strerror(errno));
+ }
+ ATF_REQUIRE_MSG(error == 0,
+ "connect() for client socket %d failed: %s",
+ i, strerror(errno));
+
+ /*
+ * We don't accept the new socket from the server socket
+ * or close the client socket, as we want the ports to
+ * remain busy. The range is small enough that this is
+ * not a problem.
+ */
+ }
+ ATF_REQUIRE_MSG(i >= 1, "No successful connections");
+ ATF_REQUIRE_MSG(got_bind_error == true, "No expected bind error");
+
+ ATF_REQUIRE(close(lsock) == 0);
+}
+ATF_TC_CLEANUP(tcp_v4mapped_bind, tc)
+{
+ restore_portrange();
+}
+
+ATF_TC(udp_v4mapped_sendto);
+ATF_TC_HEAD(udp_v4mapped_sendto, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Validate sendto() with a v4-mapped address and a v6-only socket");
+}
+ATF_TC_BODY(udp_v4mapped_sendto, tc)
+{
+ struct addrinfo ai_hint, *aip;
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ ssize_t n;
+ socklen_t salen;
+ int error, ls, s, zero;
+ short port;
+ char ch;
+
+ ls = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_REQUIRE(ls >= 0);
+
+ memset(&ai_hint, 0, sizeof(ai_hint));
+ ai_hint.ai_family = AF_INET;
+ ai_hint.ai_flags = AI_NUMERICHOST;
+ error = getaddrinfo("127.0.0.1", NULL, &ai_hint, &aip);
+ ATF_REQUIRE_MSG(error == 0, "getaddrinfo: %s", gai_strerror(error));
+ memcpy(&sin, aip->ai_addr, sizeof(sin));
+
+ error = bind(ls, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind: %s", strerror(errno));
+ salen = sizeof(sin);
+ error = getsockname(ls, (struct sockaddr *)&sin, &salen);
+ ATF_REQUIRE_MSG(error == 0,
+ "getsockname() for listen socket failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG(salen == sizeof(struct sockaddr_in),
+ "unexpected sockaddr size");
+ port = sin.sin_port;
+
+ s = socket(PF_INET6, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s >= 0);
+
+ memset(&ai_hint, 0, sizeof(ai_hint));
+ ai_hint.ai_family = AF_INET6;
+ ai_hint.ai_flags = AI_NUMERICHOST | AI_V4MAPPED;
+ error = getaddrinfo("127.0.0.1", NULL, &ai_hint, &aip);
+ ATF_REQUIRE_MSG(error == 0, "getaddrinfo: %s", gai_strerror(error));
+ memcpy(&sin6, aip->ai_addr, sizeof(sin6));
+ sin6.sin6_port = port;
+ freeaddrinfo(aip);
+
+ ch = 0x42;
+ n = sendto(s, &ch, 1, 0, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_ERRNO(EINVAL, n == -1);
+
+ zero = 0;
+ error = setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, &zero, sizeof(zero));
+ ATF_REQUIRE_MSG(error == 0,
+ "setsockopt(IPV6_V6ONLY) failed: %s", strerror(errno));
+
+ ch = 0x42;
+ n = sendto(s, &ch, 1, 0, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(n == 1, "sendto() failed: %s", strerror(errno));
+
+ ch = 0;
+ n = recv(ls, &ch, 1, 0);
+ ATF_REQUIRE_MSG(n == 1, "recv() failed: %s", strerror(errno));
+ ATF_REQUIRE(ch == 0x42);
+
+ ATF_REQUIRE(close(s) == 0);
+ ATF_REQUIRE(close(ls) == 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, tcp_v4mapped_bind);
+ ATF_TP_ADD_TC(tp, udp_v4mapped_sendto);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/ip_reass_test.c b/tests/sys/netinet/ip_reass_test.c
new file mode 100644
index 000000000000..538815bd7a2c
--- /dev/null
+++ b/tests/sys/netinet/ip_reass_test.c
@@ -0,0 +1,384 @@
+/*-
+ * Copyright (c) 2018 The FreeBSD Foundation
+ *
+ * This software was developed by Mark Johnston 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/ioctl.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+
+#include <net/bpf.h>
+#include <net/if.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+#include <netinet/ip_var.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+struct lopacket {
+ u_int family;
+ struct ip hdr;
+ char payload[];
+};
+
+static void
+update_cksum(struct ip *ip)
+{
+ size_t i;
+ uint32_t cksum;
+ uint8_t *cksump;
+ uint16_t tmp;
+
+ ip->ip_sum = 0;
+ 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);
+}
+
+static struct lopacket *
+alloc_lopacket(in_addr_t dstaddr, size_t payloadlen)
+{
+ struct ip *ip;
+ struct lopacket *packet;
+ size_t pktlen;
+
+ pktlen = sizeof(*packet) + payloadlen;
+ packet = malloc(pktlen);
+ ATF_REQUIRE(packet != NULL);
+
+ memset(packet, 0, pktlen);
+ packet->family = AF_INET;
+
+ ip = &packet->hdr;
+ ip->ip_hl = sizeof(struct ip) >> 2;
+ ip->ip_v = 4;
+ ip->ip_tos = 0;
+ ip->ip_len = htons(sizeof(*ip) + payloadlen);
+ ip->ip_id = 0;
+ ip->ip_off = 0;
+ ip->ip_ttl = 1;
+ ip->ip_p = IPPROTO_IP;
+ ip->ip_sum = 0;
+ ip->ip_src.s_addr = dstaddr;
+ ip->ip_dst.s_addr = dstaddr;
+ update_cksum(ip);
+
+ return (packet);
+}
+
+static void
+free_lopacket(struct lopacket *packet)
+{
+
+ free(packet);
+}
+
+static void
+write_lopacket(int bpffd, struct lopacket *packet)
+{
+ struct timespec ts;
+ ssize_t n;
+ size_t len;
+
+ len = sizeof(packet->family) + ntohs(packet->hdr.ip_len);
+ n = write(bpffd, packet, len);
+ ATF_REQUIRE_MSG(n >= 0, "packet write failed: %s", strerror(errno));
+ ATF_REQUIRE_MSG((size_t)n == len, "wrote %zd bytes instead of %zu",
+ n, len);
+
+ /*
+ * Loopback packets are dispatched asynchronously, give netisr some
+ * time.
+ */
+ ts.tv_sec = 0;
+ ts.tv_nsec = 5000000; /* 5ms */
+ (void)nanosleep(&ts, NULL);
+}
+
+static int
+open_lobpf(in_addr_t *addrp)
+{
+ struct ifreq ifr;
+ struct ifaddrs *ifa, *ifap;
+ int error, fd;
+
+ fd = open("/dev/bpf0", O_RDWR);
+ if (fd < 0 && errno == ENOENT)
+ atf_tc_skip("no BPF device available");
+ ATF_REQUIRE_MSG(fd >= 0, "open(/dev/bpf0): %s", strerror(errno));
+
+ error = getifaddrs(&ifap);
+ ATF_REQUIRE(error == 0);
+ for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next)
+ if ((ifa->ifa_flags & IFF_LOOPBACK) != 0 &&
+ ifa->ifa_addr->sa_family == AF_INET)
+ break;
+ if (ifa == NULL)
+ atf_tc_skip("no loopback address found");
+
+ memset(&ifr, 0, sizeof(ifr));
+ strlcpy(ifr.ifr_name, ifa->ifa_name, IFNAMSIZ);
+ error = ioctl(fd, BIOCSETIF, &ifr);
+ ATF_REQUIRE_MSG(error == 0, "ioctl(BIOCSETIF): %s", strerror(errno));
+
+ *addrp = ((struct sockaddr_in *)(void *)ifa->ifa_addr)->sin_addr.s_addr;
+
+ freeifaddrs(ifap);
+
+ return (fd);
+}
+
+static void
+get_ipstat(struct ipstat *stat)
+{
+ size_t len;
+ int error;
+
+ memset(stat, 0, sizeof(*stat));
+ len = sizeof(*stat);
+ error = sysctlbyname("net.inet.ip.stats", stat, &len, NULL, 0);
+ ATF_REQUIRE_MSG(error == 0, "sysctl(net.inet.ip.stats) failed: %s",
+ strerror(errno));
+ ATF_REQUIRE(len == sizeof(*stat));
+}
+
+#define CHECK_IP_COUNTER(oldp, newp, counter) \
+ ATF_REQUIRE_MSG((oldp)->ips_ ## counter < (newp)->ips_ ## counter, \
+ "ips_" #counter " wasn't incremented (%ju vs. %ju)", \
+ (uintmax_t)old.ips_ ## counter, (uintmax_t)new.ips_## counter);
+
+/*
+ * Make sure a fragment with MF set doesn't come after the last fragment of a
+ * packet. Make sure that multiple fragments with MF clear have the same offset
+ * and length.
+ */
+ATF_TC(ip_reass__multiple_last_fragments);
+ATF_TC_HEAD(ip_reass__multiple_last_fragments, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(ip_reass__multiple_last_fragments, tc)
+{
+ struct ipstat old, new;
+ struct ip *ip;
+ struct lopacket *packet1, *packet2, *packet3, *packet4;
+ in_addr_t addr;
+ int error, fd;
+ uint16_t ipid;
+
+ fd = open_lobpf(&addr);
+ ipid = arc4random_uniform(UINT16_MAX + 1);
+
+ packet1 = alloc_lopacket(addr, 16);
+ ip = &packet1->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(0x10);
+ update_cksum(ip);
+
+ packet2 = alloc_lopacket(addr, 16);
+ ip = &packet2->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(0x20);
+ update_cksum(ip);
+
+ packet3 = alloc_lopacket(addr, 16);
+ ip = &packet3->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(0x8);
+ update_cksum(ip);
+
+ packet4 = alloc_lopacket(addr, 32);
+ ip = &packet4->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(0x10);
+ update_cksum(ip);
+
+ write_lopacket(fd, packet1);
+
+ /* packet2 comes after packet1. */
+ get_ipstat(&old);
+ write_lopacket(fd, packet2);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ /* packet2 comes after packet1 and has MF set. */
+ packet2->hdr.ip_off = htons(IP_MF | 0x20);
+ update_cksum(&packet2->hdr);
+ get_ipstat(&old);
+ write_lopacket(fd, packet2);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ /* packet3 comes before packet1 but overlaps. */
+ get_ipstat(&old);
+ write_lopacket(fd, packet3);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ /* packet4 has the same offset as packet1 but is longer. */
+ get_ipstat(&old);
+ write_lopacket(fd, packet4);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ error = close(fd);
+ ATF_REQUIRE(error == 0);
+ free_lopacket(packet1);
+ free_lopacket(packet2);
+ free_lopacket(packet3);
+ free_lopacket(packet4);
+}
+
+/*
+ * Make sure that we reject zero-length fragments.
+ */
+ATF_TC(ip_reass__zero_length_fragment);
+ATF_TC_HEAD(ip_reass__zero_length_fragment, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(ip_reass__zero_length_fragment, tc)
+{
+ struct ipstat old, new;
+ struct ip *ip;
+ struct lopacket *packet1, *packet2;
+ in_addr_t addr;
+ int error, fd;
+ uint16_t ipid;
+
+ fd = open_lobpf(&addr);
+ ipid = arc4random_uniform(UINT16_MAX + 1);
+
+ /*
+ * Create two packets, one with MF set, one without.
+ */
+ packet1 = alloc_lopacket(addr, 0);
+ ip = &packet1->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(IP_MF | 0x10);
+ update_cksum(ip);
+
+ packet2 = alloc_lopacket(addr, 0);
+ ip = &packet2->hdr;
+ ip->ip_id = ~ipid;
+ ip->ip_off = htons(0x10);
+ update_cksum(ip);
+
+ get_ipstat(&old);
+ write_lopacket(fd, packet1);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, toosmall);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ get_ipstat(&old);
+ write_lopacket(fd, packet2);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, toosmall);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ error = close(fd);
+ ATF_REQUIRE(error == 0);
+ free_lopacket(packet1);
+ free_lopacket(packet2);
+}
+
+ATF_TC(ip_reass__large_fragment);
+ATF_TC_HEAD(ip_reass__large_fragment, tc)
+{
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(ip_reass__large_fragment, tc)
+{
+ struct ipstat old, new;
+ struct ip *ip;
+ struct lopacket *packet1, *packet2;
+ in_addr_t addr;
+ int error, fd;
+ uint16_t ipid;
+
+ fd = open_lobpf(&addr);
+ ipid = arc4random_uniform(UINT16_MAX + 1);
+
+ /*
+ * Create two packets, one with MF set, one without.
+ *
+ * 16 + (0x1fff << 3) > IP_MAXPACKET, so these should fail the check.
+ */
+ packet1 = alloc_lopacket(addr, 16);
+ ip = &packet1->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(IP_MF | 0x1fff);
+ update_cksum(ip);
+
+ packet2 = alloc_lopacket(addr, 16);
+ ip = &packet2->hdr;
+ ip->ip_id = ipid;
+ ip->ip_off = htons(0x1fff);
+ update_cksum(ip);
+
+ get_ipstat(&old);
+ write_lopacket(fd, packet1);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, toolong);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ get_ipstat(&old);
+ write_lopacket(fd, packet2);
+ get_ipstat(&new);
+ CHECK_IP_COUNTER(&old, &new, toolong);
+ CHECK_IP_COUNTER(&old, &new, fragdropped);
+
+ error = close(fd);
+ ATF_REQUIRE(error == 0);
+ free_lopacket(packet1);
+ free_lopacket(packet2);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, ip_reass__multiple_last_fragments);
+ ATF_TP_ADD_TC(tp, ip_reass__zero_length_fragment);
+ ATF_TP_ADD_TC(tp, ip_reass__large_fragment);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/libalias/1_instance.c b/tests/sys/netinet/libalias/1_instance.c
new file mode 100644
index 000000000000..842acb41bb90
--- /dev/null
+++ b/tests/sys/netinet/libalias/1_instance.c
@@ -0,0 +1,119 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <atf-c.h>
+#include <alias.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+ATF_TC(2_destroynull);
+ATF_TC_HEAD(2_destroynull, env)
+{
+ atf_tc_set_md_var(env, "descr", "Destroy the NULL instance");
+}
+ATF_TC_BODY(2_destroynull, dummy)
+{
+ atf_tc_expect_death("Code expects valid pointer.");
+ LibAliasUninit(NULL);
+}
+
+ATF_TC(1_singleinit);
+ATF_TC_HEAD(1_singleinit, env)
+{
+ atf_tc_set_md_var(env, "descr", "Create an instance");
+}
+ATF_TC_BODY(1_singleinit, dummy)
+{
+ struct libalias *la;
+
+ la = LibAliasInit(NULL);
+ ATF_CHECK_MSG(la != NULL, "Creating an instance failed.");
+ LibAliasUninit(la);
+}
+
+ATF_TC(3_multiinit);
+ATF_TC_HEAD(3_multiinit, env)
+{
+ atf_tc_set_md_var(env, "descr", "Recreate an instance multiple times");
+}
+ATF_TC_BODY(3_multiinit, dummy)
+{
+ struct libalias *la;
+ int i;
+
+ la = LibAliasInit(NULL);
+ for(i = 1; i < 30; i++) {
+ struct libalias *lo = la;
+
+ la = LibAliasInit(la);
+ ATF_CHECK_MSG(la == lo, "Recreating moved the instance around: %d", i);
+ }
+ LibAliasUninit(la);
+}
+
+ATF_TC(4_multiinstance);
+ATF_TC_HEAD(4_multiinstance, env)
+{
+ atf_tc_set_md_var(env, "descr", "Create and destoy multiple instances.");
+}
+ATF_TC_BODY(4_multiinstance, dummy)
+{
+ struct libalias *la[300];
+ int const num_instances = sizeof(la) / sizeof(*la);
+ int i;
+
+ for (i = 0; i < num_instances; i++) {
+ la[i] = LibAliasInit(NULL);
+ ATF_CHECK_MSG(la[i] != NULL, "Creating instance %d failed.", i);
+ }
+
+ qsort(la, num_instances, sizeof(*la), randcmp);
+
+ for (i = 0; i < num_instances; i++)
+ LibAliasUninit(la[i]);
+}
+
+ATF_TP_ADD_TCS(instance)
+{
+ /* Use "dd if=/dev/random bs=2 count=1 | od -x" to reproduce */
+ srand(0x5ac4);
+
+ ATF_TP_ADD_TC(instance, 2_destroynull);
+ ATF_TP_ADD_TC(instance, 1_singleinit);
+ ATF_TP_ADD_TC(instance, 3_multiinit);
+ ATF_TP_ADD_TC(instance, 4_multiinstance);
+
+ return atf_no_error();
+}
diff --git a/tests/sys/netinet/libalias/2_natout.c b/tests/sys/netinet/libalias/2_natout.c
new file mode 100644
index 000000000000..24ca06d11bf4
--- /dev/null
+++ b/tests/sys/netinet/libalias/2_natout.c
@@ -0,0 +1,547 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <atf-c.h>
+#include <alias.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+ATF_TC_WITHOUT_HEAD(1_simplemasq);
+ATF_TC_BODY(1_simplemasq, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *pip;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, 0, ~0);
+
+ pip = ip_packet(254, 64);
+ NAT_CHECK(pip, prv1, ext, masq);
+ NAT_CHECK(pip, prv2, ext, masq);
+ NAT_CHECK(pip, prv3, ext, masq);
+ NAT_CHECK(pip, cgn, ext, masq);
+ NAT_CHECK(pip, pub, ext, masq);
+
+ free(pip);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(2_unregistered);
+ATF_TC_BODY(2_unregistered, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *pip;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_UNREGISTERED_ONLY, ~0);
+
+ pip = ip_packet(254, 64);
+ NAT_CHECK(pip, prv1, ext, masq);
+ NAT_CHECK(pip, prv2, ext, masq);
+ NAT_CHECK(pip, prv3, ext, masq);
+ NAT_CHECK(pip, cgn, ext, cgn);
+ NAT_CHECK(pip, pub, ext, pub);
+
+ /*
+ * State is only for new connections
+ * Because they are now active,
+ * the mode setting should be ignored
+ */
+ LibAliasSetMode(la, 0, PKT_ALIAS_UNREGISTERED_ONLY);
+ NAT_CHECK(pip, prv1, ext, masq);
+ NAT_CHECK(pip, prv2, ext, masq);
+ NAT_CHECK(pip, prv3, ext, masq);
+ NAT_CHECK(pip, cgn, ext, cgn);
+ NAT_CHECK(pip, pub, ext, pub);
+
+ free(pip);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(3_cgn);
+ATF_TC_BODY(3_cgn, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *pip;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_UNREGISTERED_CGN, ~0);
+
+ pip = ip_packet(254, 64);
+ NAT_CHECK(pip, prv1, ext, masq);
+ NAT_CHECK(pip, prv2, ext, masq);
+ NAT_CHECK(pip, prv3, ext, masq);
+ NAT_CHECK(pip, cgn, ext, masq);
+ NAT_CHECK(pip, pub, ext, pub);
+
+ /*
+ * State is only for new connections
+ * Because they are now active,
+ * the mode setting should be ignored
+ */
+ LibAliasSetMode(la, 0, PKT_ALIAS_UNREGISTERED_CGN);
+ NAT_CHECK(pip, prv1, ext, masq);
+ NAT_CHECK(pip, prv2, ext, masq);
+ NAT_CHECK(pip, prv3, ext, masq);
+ NAT_CHECK(pip, cgn, ext, masq);
+ NAT_CHECK(pip, pub, ext, pub);
+
+ free(pip);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(4_udp);
+ATF_TC_BODY(4_udp, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *pi;
+ struct udphdr *ui, *uo;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t aport;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, 0, ~0);
+
+ /* Query from prv1 */
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ /* should use a different external port */
+ ATF_CHECK(aport != sport);
+
+ /* Response */
+ pi = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(pi, ui, ext, dport, masq, aport, prv1, sport);
+
+ /* Query from different source with same ports */
+ UDP_NAT_CHECK(po, uo, prv2, sport, ext, dport, masq);
+ /* should use a different external port */
+ ATF_CHECK(uo->uh_sport != htons(aport));
+
+ /* Response to prv2 */
+ ui->uh_dport = uo->uh_sport;
+ UDP_UNNAT_CHECK(pi, ui, ext, dport, masq, htons(uo->uh_sport), prv2, sport);
+
+ /* Response to prv1 again */
+ UDP_UNNAT_CHECK(pi, ui, ext, dport, masq, aport, prv1, sport);
+
+ free(pi);
+ free(po);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(5_sameport);
+ATF_TC_BODY(5_sameport, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *p;
+ struct udphdr *u;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t aport;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_SAME_PORTS, ~0);
+
+ /* Query from prv1 */
+ p = ip_packet(0, 64);
+ UDP_NAT_CHECK(p, u, prv1, sport, ext, dport, masq);
+ aport = ntohs(u->uh_sport);
+ /* should use the same external port */
+ ATF_CHECK(aport == sport);
+
+ /* Query from different source with same ports */
+ UDP_NAT_CHECK(p, u, prv2, sport, ext, dport, masq);
+ /* should use a different external port */
+ ATF_CHECK(u->uh_sport != htons(aport));
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(6_cleartable);
+ATF_TC_BODY(6_cleartable, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po, *pi;
+ struct udphdr *ui __unused, *uo;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t aport;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_RESET_ON_ADDR_CHANGE, ~0);
+ LibAliasSetMode(la, PKT_ALIAS_SAME_PORTS, PKT_ALIAS_SAME_PORTS);
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, PKT_ALIAS_DENY_INCOMING);
+
+ /* Query from prv1 */
+ po = ip_packet(0, 64);
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ /* should use the same external port */
+ ATF_CHECK(aport == sport);
+
+ /* Response */
+ pi = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(po, uo, ext, dport, masq, aport, prv1, sport);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* Response to prv1 again -> DENY_INCOMING */
+ UDP_UNNAT_FAIL(pi, ui, ext, dport, masq, aport);
+
+ /* Query from different source with same ports */
+ UDP_NAT_CHECK(po, uo, prv2, sport, ext, dport, masq);
+ /* should use the same external port, because it's free */
+ ATF_CHECK(uo->uh_sport == htons(aport));
+
+ /* Response to prv2 */
+ UDP_UNNAT_CHECK(po, uo, ext, dport, masq, htons(uo->uh_sport), prv2, sport);
+
+ free(pi);
+ free(po);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(7_stress);
+ATF_TC_BODY(7_stress, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *p;
+ struct udphdr *u;
+ struct {
+ struct in_addr src, dst;
+ uint16_t sport, dport, aport;
+ } *batch;
+ size_t const batch_size = 1200;
+ size_t const rounds = 25;
+ size_t i, j;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+
+ p = ip_packet(0, 64);
+
+ batch = calloc(batch_size, sizeof(*batch));
+ ATF_REQUIRE(batch != NULL);
+ for (j = 0; j < rounds; j++) {
+ for (i = 0; i < batch_size; i++) {
+ struct in_addr s, d;
+ switch (i&3) {
+ case 0: s = prv1; d = ext; break;
+ case 1: s = prv2; d = pub; break;
+ case 2: s = prv3; d = ext; break;
+ case 3: s = cgn; d = pub; break;
+ }
+ s.s_addr &= htonl(0xffff0000);
+ d.s_addr &= htonl(0xffff0000);
+ batch[i].src.s_addr = s.s_addr | htonl(rand_range(0, 0xffff));
+ batch[i].dst.s_addr = d.s_addr | htonl(rand_range(0, 0xffff));
+ batch[i].sport = rand_range(1000, 60000);
+ batch[i].dport = rand_range(1000, 60000);
+ }
+
+ for (i = 0; i < batch_size; i++) {
+ UDP_NAT_CHECK(p, u,
+ batch[i].src, batch[i].sport,
+ batch[i].dst, batch[i].dport,
+ masq);
+ batch[i].aport = htons(u->uh_sport);
+ }
+
+ qsort(batch, batch_size, sizeof(*batch), randcmp);
+
+ for (i = 0; i < batch_size; i++) {
+ UDP_UNNAT_CHECK(p, u,
+ batch[i].dst, batch[i].dport,
+ masq, batch[i].aport,
+ batch[i].src, batch[i].sport);
+ }
+ }
+
+ free(batch);
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(8_portrange);
+ATF_TC_BODY(8_portrange, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct ip *po;
+ struct udphdr *uo;
+ uint16_t sport = 0x1234;
+ uint16_t dport = 0x5678;
+ uint16_t aport;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, 0, ~0);
+ po = ip_packet(0, 64);
+
+ LibAliasSetAliasPortRange(la, 0, 0); /* reinit like ipfw */
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ ATF_CHECK(aport >= 0x8000);
+
+ /* Different larger range */
+ LibAliasSetAliasPortRange(la, 2000, 3000);
+ dport++;
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ ATF_CHECK(aport >= 2000 && aport < 3000);
+
+ /* Different small range (contains two ports) */
+ LibAliasSetAliasPortRange(la, 4000, 4001);
+ dport++;
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ ATF_CHECK(aport >= 4000 && aport <= 4001);
+
+ sport++;
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ ATF_CHECK(aport >= 4000 && aport <= 4001);
+
+ /* Third port not available in the range */
+ sport++;
+ UDP_NAT_FAIL(po, uo, prv1, sport, ext, dport);
+
+ /* Back to normal */
+ LibAliasSetAliasPortRange(la, 0, 0);
+ dport++;
+ UDP_NAT_CHECK(po, uo, prv1, sport, ext, dport, masq);
+ aport = ntohs(uo->uh_sport);
+ ATF_CHECK(aport >= 0x8000);
+
+ free(po);
+ 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 */
+ srand(0x0b61);
+
+ ATF_TP_ADD_TC(natout, 1_simplemasq);
+ ATF_TP_ADD_TC(natout, 2_unregistered);
+ ATF_TP_ADD_TC(natout, 3_cgn);
+ ATF_TP_ADD_TC(natout, 4_udp);
+ ATF_TP_ADD_TC(natout, 5_sameport);
+ 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/3_natin.c b/tests/sys/netinet/libalias/3_natin.c
new file mode 100644
index 000000000000..3bc088ce3da9
--- /dev/null
+++ b/tests/sys/netinet/libalias/3_natin.c
@@ -0,0 +1,381 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <atf-c.h>
+#include <alias.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "util.h"
+
+ATF_TC_WITHOUT_HEAD(1_portforward);
+ATF_TC_BODY(1_portforward, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf1, *pf2, *pf3, *pf4;
+ struct ip *p;
+ struct udphdr *u;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_RESET_ON_ADDR_CHANGE, ~0);
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, PKT_ALIAS_DENY_INCOMING);
+
+ /*
+ * Fully specified
+ */
+ pf1 = LibAliasRedirectPort(la, prv1, ntohs(0x1234), ext, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf1 != NULL);
+
+ p = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+ /* try again */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+ /* different source */
+ UDP_UNNAT_FAIL(p, u, pub, 0x5678, masq, 0xabcd);
+ UDP_UNNAT_FAIL(p, u, ext, 0xdead, masq, 0xabcd);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* delete and try again */
+ LibAliasRedirectDelete(la, pf1);
+ UDP_UNNAT_FAIL(p, u, ext, 0x5678, masq, 0xabcd);
+
+ /*
+ * Any external port
+ */
+ pf2 = LibAliasRedirectPort(la, prv2, ntohs(0x1234), ext, ntohs(0), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf2 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv2, 0x1234);
+ /* try again */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv2, 0x1234);
+ /* different source */
+ UDP_UNNAT_FAIL(p, u, pub, 0x5678, masq, 0xabcd);
+ UDP_UNNAT_CHECK(p, u, ext, 0xdead, masq, 0xabcd, prv2, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* delete and try again */
+ LibAliasRedirectDelete(la, pf2);
+ UDP_UNNAT_FAIL(p, u, ext, 0x5678, masq, 0xabcd);
+
+ /*
+ * Any external host
+ */
+ pf3 = LibAliasRedirectPort(la, prv3, ntohs(0x1234), ANY_ADDR, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf3 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv3, 0x1234);
+ /* try again */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv3, 0x1234);
+ /* different source */
+ UDP_UNNAT_CHECK(p, u, pub, 0x5678, masq, 0xabcd, prv3, 0x1234);
+ UDP_UNNAT_FAIL(p, u, ext, 0xdead, masq, 0xabcd);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* delete and try again */
+ LibAliasRedirectDelete(la, pf3);
+ UDP_UNNAT_FAIL(p, u, ext, 0x5678, masq, 0xabcd);
+
+ /*
+ * Any external host, any port
+ */
+ pf4 = LibAliasRedirectPort(la, cgn, ntohs(0x1234), ANY_ADDR, ntohs(0), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf4 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, cgn, 0x1234);
+ /* try again */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, cgn, 0x1234);
+ /* different source */
+ UDP_UNNAT_CHECK(p, u, pub, 0x5678, masq, 0xabcd, cgn, 0x1234);
+ UDP_UNNAT_CHECK(p, u, ext, 0xdead, masq, 0xabcd, cgn, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* delete and try again */
+ LibAliasRedirectDelete(la, pf4);
+ UDP_UNNAT_FAIL(p, u, ext, 0x5678, masq, 0xabcd);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(2_portoverlap);
+ATF_TC_BODY(2_portoverlap, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf1, *pf2, *pf3, *pf4;
+ struct ip *p;
+ struct udphdr *u;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_RESET_ON_ADDR_CHANGE, ~0);
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, PKT_ALIAS_DENY_INCOMING);
+
+ /*
+ * Fully specified
+ */
+ pf1 = LibAliasRedirectPort(la, prv2, ntohs(0x1234), ext, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf1 != NULL);
+
+ p = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv2, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /*
+ * Fully specified (override)
+ */
+ pf1 = LibAliasRedirectPort(la, prv1, ntohs(0x1234), ext, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf1 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /*
+ * Any external port
+ */
+ pf2 = LibAliasRedirectPort(la, prv2, ntohs(0x1234), ext, ntohs(0), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf2 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5679, masq, 0xabcd, prv2, 0x1234);
+ /* more specific rule wins */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /*
+ * Any external host
+ */
+ pf3 = LibAliasRedirectPort(la, prv3, ntohs(0x1234), ANY_ADDR, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf3 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, pub, 0x5678, masq, 0xabcd, prv3, 0x1234);
+ /* more specific rule wins */
+ UDP_UNNAT_CHECK(p, u, ext, 0x5679, masq, 0xabcd, prv2, 0x1234);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /*
+ * Any external host, any port
+ */
+ pf4 = LibAliasRedirectPort(la, cgn, ntohs(0x1234), ANY_ADDR, ntohs(0), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf4 != NULL);
+
+ UDP_UNNAT_CHECK(p, u, prv1, 0x5679, masq, 0xabcd, cgn, 0x1234);
+ /* more specific rule wins */
+ UDP_UNNAT_CHECK(p, u, pub, 0x5678, masq, 0xabcd, prv3, 0x1234);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5679, masq, 0xabcd, prv2, 0x1234);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(3_redirectany);
+ATF_TC_BODY(3_redirectany, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf;
+ struct ip *p;
+ struct udphdr *u;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, ~0);
+ p = ip_packet(0, 64);
+
+ pf = LibAliasRedirectPort(la, prv1, ntohs(0x1234), ANY_ADDR, 0, ANY_ADDR, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf != NULL);
+
+ LibAliasSetAddress(la, masq);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+ UDP_UNNAT_FAIL(p, u, pub, 0x5678, pub, 0xabcd);
+
+ LibAliasSetAddress(la, pub);
+ UDP_UNNAT_CHECK(p, u, pub, 0x5679, pub, 0xabcd, prv1, 0x1234);
+ UDP_UNNAT_FAIL(p, u, ext, 0x5679, masq, 0xabcd);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(4_redirectaddr);
+ATF_TC_BODY(4_redirectaddr, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf1, *pf2;
+ struct ip *p;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetAddress(la, masq);
+ pf1 = LibAliasRedirectAddr(la, prv1, pub);
+ ATF_REQUIRE(pf1 != NULL);
+
+ p = ip_packet(254, 64);
+ UNNAT_CHECK(p, ext, pub, prv1);
+ UNNAT_CHECK(p, ext, masq, masq);
+
+ pf2 = LibAliasRedirectAddr(la, prv2, pub);
+ ATF_REQUIRE(pf2 != NULL);
+ UNNAT_CHECK(p, ext, pub, prv1);
+ p->ip_p = 253; /* new flows */
+ UNNAT_CHECK(p, ext, pub, prv2);
+ UNNAT_CHECK(p, ext, masq, masq);
+
+ p->ip_p = 252; /* new flows */
+ NAT_CHECK(p, prv1, ext, pub);
+ NAT_CHECK(p, prv2, ext, pub);
+ NAT_CHECK(p, prv3, ext, masq);
+
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, ~0);
+ p->ip_p = 251; /* new flows */
+ UNNAT_FAIL(p, ext, pub);
+ UNNAT_FAIL(p, ext, masq);
+
+ /* unhide older version */
+ LibAliasRedirectDelete(la, pf2);
+ LibAliasSetMode(la, 0, ~0);
+ p->ip_p = 250; /* new flows */
+ UNNAT_CHECK(p, ext, pub, prv1);
+
+ p->ip_p = 249; /* new flows */
+ NAT_CHECK(p, prv1, ext, pub);
+ NAT_CHECK(p, prv2, ext, masq);
+ NAT_CHECK(p, prv3, ext, masq);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(5_lsnat);
+ATF_TC_BODY(5_lsnat, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf;
+ struct ip *p;
+ struct udphdr *u;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetMode(la, 0, ~0);
+ p = ip_packet(0, 64);
+
+ pf = LibAliasRedirectPort(la, cgn, ntohs(0xdead), ANY_ADDR, 0, masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf != NULL);
+
+ ATF_REQUIRE(0 == LibAliasAddServer(la, pf, prv1, ntohs(0x1234)));
+ ATF_REQUIRE(0 == LibAliasAddServer(la, pf, prv2, ntohs(0x2345)));
+ ATF_REQUIRE(0 == LibAliasAddServer(la, pf, prv3, ntohs(0x3456)));
+
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv3, 0x3456);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5679, masq, 0xabcd, prv2, 0x2345);
+ UDP_UNNAT_CHECK(p, u, ext, 0x567a, masq, 0xabcd, prv1, 0x1234);
+ UDP_UNNAT_CHECK(p, u, ext, 0x567b, masq, 0xabcd, prv3, 0x3456);
+ UDP_UNNAT_CHECK(p, u, ext, 0x567c, masq, 0xabcd, prv2, 0x2345);
+ UDP_UNNAT_CHECK(p, u, ext, 0x567d, masq, 0xabcd, prv1, 0x1234);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TC_WITHOUT_HEAD(6_oneshot);
+ATF_TC_BODY(6_oneshot, dummy)
+{
+ struct libalias *la = LibAliasInit(NULL);
+ struct alias_link *pf;
+ struct ip *p;
+ struct udphdr *u;
+
+ ATF_REQUIRE(la != NULL);
+ LibAliasSetMode(la, 0, ~0);
+ LibAliasSetMode(la, PKT_ALIAS_RESET_ON_ADDR_CHANGE, ~0);
+ LibAliasSetMode(la, PKT_ALIAS_DENY_INCOMING, PKT_ALIAS_DENY_INCOMING);
+
+ pf = LibAliasRedirectPort(la, prv1, ntohs(0x1234), ANY_ADDR, 0, masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf != NULL);
+ /* only for fully specified links */
+ ATF_CHECK(-1 == LibAliasRedirectDynamic(la, pf));
+ LibAliasRedirectDelete(la, pf);
+
+ pf = LibAliasRedirectPort(la, prv1, ntohs(0x1234), ext, ntohs(0x5678), masq, ntohs(0xabcd), IPPROTO_UDP);
+ ATF_REQUIRE(pf != NULL);
+ ATF_CHECK(0 == LibAliasRedirectDynamic(la, pf));
+
+ p = ip_packet(0, 64);
+ UDP_UNNAT_CHECK(p, u, ext, 0x5678, masq, 0xabcd, prv1, 0x1234);
+
+ /* clear table by keeping the address */
+ LibAliasSetAddress(la, ext);
+ LibAliasSetAddress(la, masq);
+
+ /* does not work anymore */
+ UDP_UNNAT_FAIL(p, u, ext, 0x5678, masq, 0xabcd);
+
+ free(p);
+ LibAliasUninit(la);
+}
+
+ATF_TP_ADD_TCS(natin)
+{
+ /* Use "dd if=/dev/random bs=2 count=1 | od -x" to reproduce */
+ srand(0xe859);
+
+ ATF_TP_ADD_TC(natin, 1_portforward);
+ ATF_TP_ADD_TC(natin, 2_portoverlap);
+ ATF_TP_ADD_TC(natin, 3_redirectany);
+ ATF_TP_ADD_TC(natin, 4_redirectaddr);
+ ATF_TP_ADD_TC(natin, 5_lsnat);
+ ATF_TP_ADD_TC(natin, 6_oneshot);
+
+ return atf_no_error();
+}
diff --git a/tests/sys/netinet/libalias/Makefile b/tests/sys/netinet/libalias/Makefile
new file mode 100644
index 000000000000..43bef996fae7
--- /dev/null
+++ b/tests/sys/netinet/libalias/Makefile
@@ -0,0 +1,31 @@
+PACKAGE= tests
+
+TESTSDIR= ${TESTSBASE}/sys/netinet/libalias
+BINDIR= ${TESTSDIR}
+
+ATF_TESTS_C+= 1_instance \
+ 2_natout \
+ 3_natin \
+
+PROGS+= perf
+
+LIBADD+= alias
+
+SRCS.1_instance=1_instance.c util.c
+SRCS.2_natout= 2_natout.c util.c
+SRCS.3_natin= 3_natin.c util.c
+SRCS.perf= perf.c util.c
+
+.include <bsd.test.mk>
+
+#
+# Testing during development
+#
+test: all
+ cd ${.OBJDIR}; kyua test
+
+report:
+ cd ${.OBJDIR}; kyua report
+
+report-v:
+ cd ${.OBJDIR}; kyua report --verbose
diff --git a/tests/sys/netinet/libalias/perf.c b/tests/sys/netinet/libalias/perf.c
new file mode 100644
index 000000000000..07e73612a6dd
--- /dev/null
+++ b/tests/sys/netinet/libalias/perf.c
@@ -0,0 +1,308 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <stdio.h>
+#include <stdlib.h>
+#include <strings.h>
+#include <sys/time.h>
+#include "util.h"
+#include <alias.h>
+
+static void usage(void) __dead2;
+
+#define timevalcmp(tv, uv, cmp) \
+ (((tv).tv_sec == (uv).tv_sec) \
+ ? ((tv).tv_usec cmp (uv).tv_usec) \
+ : ((tv).tv_sec cmp (uv).tv_sec))
+
+#define timevaldiff(n, o) (float) \
+ (((n).tv_sec - (o).tv_sec)*1000000l + \
+ ((n).tv_usec - (o).tv_usec))
+
+#define check_timeout() do { \
+ if (check_timeout_cnt++ > 1000) { \
+ check_timeout_cnt = 0; \
+ gettimeofday(&now, NULL); \
+ if (timevalcmp(now, timeout, >=)) \
+ goto out; \
+ } } while(0)
+
+static void
+usage(void) {
+ printf("Usage: perf [max_seconds [batch_size [random_size [attack_size [redir_size]]]]]\n");
+ exit(1);
+}
+
+int main(int argc, char ** argv)
+{
+ struct libalias *la;
+ struct timeval timeout, now, start;
+ struct ip *p;
+ struct udphdr *u;
+ struct {
+ struct in_addr src, dst;
+ uint16_t sport, dport, aport;
+ } *batch;
+ struct {
+ unsigned long ok, fail;
+ } nat, usenat, unnat, random, attack;
+ int i, round, check_timeout_cnt = 0;
+ int max_seconds = 90, batch_size = 2000,
+ random_size = 1000, attack_size = 1000,
+ redir_size = 2000;
+
+ if (argc >= 2) {
+ char * end;
+
+ max_seconds = strtol(argv[1], &end, 10);
+ if (max_seconds < 2 || end[0] != '\0')
+ usage();
+ }
+ if (argc > 2 && (batch_size = atoi(argv[2])) < 0) usage();
+ if (argc > 3 && (random_size = atoi(argv[3])) < 0) usage();
+ if (argc > 4 && (attack_size = atoi(argv[4])) < 0) usage();
+ if (argc > 5 && (redir_size = atoi(argv[5])) < 0) usage();
+
+ printf("Running perfomance test with parameters:\n");
+ printf(" Maximum Runtime (max_seconds) = %d\n", max_seconds);
+ printf(" Amount of valid connections (batch_size) = %d\n", batch_size);
+ printf(" Amount of random, incoming packets (batch_size) = %d\n", random_size);
+ printf(" Repeat count of a random, incoming packet (attack_size) = %d\n", attack_size);
+ printf(" Amount of open port forwardings (redir_size) = %d\n", redir_size);
+ printf("\n");
+
+ if (NULL == (la = LibAliasInit(NULL))) {
+ perror("LibAliasInit");
+ return -1;
+ }
+
+ bzero(&nat, sizeof(nat));
+ bzero(&usenat, sizeof(usenat));
+ bzero(&unnat, sizeof(unnat));
+ bzero(&random, sizeof(random));
+ bzero(&attack, sizeof(attack));
+
+ LibAliasSetAddress(la, masq);
+ LibAliasSetMode(la, PKT_ALIAS_SAME_PORTS | PKT_ALIAS_DENY_INCOMING, ~0);
+
+ prv1.s_addr &= htonl(0xffff0000);
+ ext.s_addr &= htonl(0xffff0000);
+
+ for (i = 0; i < redir_size; i++) {
+ int aport = htons(rand_range(1000, 2000));
+ int sport = htons(rand_range(1000, 2000));
+
+ prv2.s_addr &= htonl(0xffff0000);
+ prv2.s_addr |= rand_range(0, 0xffff);
+ LibAliasRedirectPort(la, prv2, sport, ANY_ADDR, 0, masq, aport, IPPROTO_UDP);
+ }
+
+ p = ip_packet(0, 64);
+ u = set_udp(p, 0, 0);
+
+ if (NULL == (batch = calloc(batch_size, sizeof(*batch)))) {
+ perror("calloc(batch)");
+ return -1;
+ }
+
+ gettimeofday(&timeout, NULL);
+ timeout.tv_sec += max_seconds;
+
+ printf("RND SECOND newNAT RANDOM ATTACK useNAT\n");
+ for (round = 0; ; round++) {
+ int res, cnt;
+
+ printf("%3d ", round+1);
+
+ gettimeofday(&start, NULL);
+ printf("%6.1f ", max_seconds - timevaldiff(timeout, start)/1000000.0f);
+ for (cnt = i = 0; i < batch_size; i++, cnt++) {
+ batch[i].src.s_addr = prv1.s_addr | htonl(rand_range(0, 0xffff));
+ batch[i].dst.s_addr = ext.s_addr | htonl(rand_range(0, 0xffff));
+ batch[i].sport = rand_range(1000, 60000);
+ batch[i].dport = rand_range(1000, 60000);
+
+ p->ip_src = batch[i].src;
+ p->ip_dst = batch[i].dst;
+ u = set_udp(p, batch[i].sport, batch[i].dport);
+
+ res = LibAliasOut(la, p, 64);
+ batch[i].aport = htons(u->uh_sport);
+
+ if (res == PKT_ALIAS_OK &&
+ u->uh_dport == htons(batch[i].dport) &&
+ addr_eq(p->ip_dst, batch[i].dst) &&
+ addr_eq(p->ip_src, masq))
+ nat.ok++;
+ else
+ nat.fail++;
+
+ check_timeout();
+ }
+ gettimeofday(&now, NULL);
+ if (cnt > 0)
+ printf("%6.2f ", timevaldiff(now, start) / cnt);
+ else
+ printf("------ ");
+
+ start = now;
+ for (cnt = i = 0; i < random_size; i++, cnt++) {
+ p->ip_src.s_addr = ext.s_addr & htonl(0xfff00000);
+ p->ip_src.s_addr |= htonl(rand_range(0, 0xffff));
+ p->ip_dst = masq;
+ u = set_udp(p, rand_range(1, 0xffff), rand_range(1, 0xffff));
+
+ res = LibAliasIn(la, p, 64);
+
+ if (res == PKT_ALIAS_OK)
+ random.ok++;
+ else
+ random.fail++;
+
+ check_timeout();
+ }
+ gettimeofday(&now, NULL);
+ if (cnt > 0)
+ printf("%6.2f ", timevaldiff(now, start) / cnt);
+ else
+ printf("------ ");
+
+ start = now;
+ p->ip_src.s_addr = ext.s_addr & htonl(0xfff00000);
+ p->ip_src.s_addr |= htonl(rand_range(0, 0xffff));
+ p->ip_dst = masq;
+ u = set_udp(p, rand_range(1, 0xffff), rand_range(1, 0xffff));
+ for (cnt = i = 0; i < attack_size; i++, cnt++) {
+ res = LibAliasIn(la, p, 64);
+
+ if (res == PKT_ALIAS_OK)
+ attack.ok++;
+ else
+ attack.fail++;
+
+ check_timeout();
+ }
+ gettimeofday(&now, NULL);
+ if (cnt > 0)
+ printf("%6.2f ", timevaldiff(now, start) / cnt);
+ else
+ printf("------ ");
+
+ qsort(batch, batch_size, sizeof(*batch), randcmp);
+
+ gettimeofday(&start, NULL);
+ for (cnt = i = 0; i < batch_size; i++) {
+ int j;
+
+ /* random communication length */
+ for(j = rand_range(1, 150); j-- > 0; cnt++) {
+ int k;
+
+ /* a random flow out of rolling window */
+ k = rand_range(i, i + 25);
+ if (k >= batch_size)
+ k = i;
+
+ /* 10% outgoing, 90% incoming */
+ if (rand_range(0, 100) > 10) {
+ p->ip_src = batch[k].dst;
+ p->ip_dst = masq;
+ u = set_udp(p, batch[k].dport, batch[k].aport);
+
+ res = LibAliasIn(la, p, 64);
+ if (res == PKT_ALIAS_OK &&
+ u->uh_sport == htons(batch[k].dport) &&
+ u->uh_dport == htons(batch[k].sport) &&
+ addr_eq(p->ip_dst, batch[k].src) &&
+ addr_eq(p->ip_src, batch[k].dst))
+ unnat.ok++;
+ else
+ unnat.fail++;
+ } else {
+ p->ip_src = batch[k].src;
+ p->ip_dst = batch[k].dst;
+ u = set_udp(p, batch[k].sport, batch[k].dport);
+
+ res = LibAliasOut(la, p, 64);
+ if (res == PKT_ALIAS_OK &&
+ u->uh_sport == htons(batch[k].aport) &&
+ u->uh_dport == htons(batch[k].dport) &&
+ addr_eq(p->ip_dst, batch[k].dst) &&
+ addr_eq(p->ip_src, masq))
+ usenat.ok++;
+ else
+ usenat.fail++;
+ }
+ check_timeout();
+ }
+ }
+ gettimeofday(&now, NULL);
+ if (cnt > 0)
+ printf("%6.2f ", timevaldiff(now, start) / cnt);
+ else
+ printf("------ ");
+
+ printf("\n");
+ }
+out:
+ printf("\n\n");
+ free(batch);
+ free(p);
+
+ printf("Results\n");
+ printf(" Rounds : %9u\n", round);
+ printf("newNAT ok : %9lu\n", nat.ok);
+ printf("newNAT fail: %9lu\n", nat.fail);
+ printf("useNAT ok : %9lu (out)\n", usenat.ok);
+ printf("useNAT fail: %9lu (out)\n", usenat.fail);
+ printf("useNAT ok : %9lu (in)\n", unnat.ok);
+ printf("useNAT fail: %9lu (in)\n", unnat.fail);
+ printf("RANDOM ok : %9lu\n", random.ok);
+ printf("RANDOM fail: %9lu\n", random.fail);
+ printf("ATTACK ok : %9lu\n", attack.ok);
+ printf("ATTACK fail: %9lu\n", attack.fail);
+ printf(" ---------\n");
+ printf(" Total: %9lu\n",
+ nat.ok + nat.fail +
+ unnat.ok + unnat.fail +
+ usenat.ok + usenat.fail +
+ random.ok + random.fail +
+ attack.ok + attack.fail);
+
+ gettimeofday(&start, NULL);
+ printf("\n Cleanup : ");
+ LibAliasUninit(la);
+ gettimeofday(&now, NULL);
+ printf("%.2fs\n", timevaldiff(now, start)/1000000l);
+ return (0);
+}
diff --git a/tests/sys/netinet/libalias/util.c b/tests/sys/netinet/libalias/util.c
new file mode 100644
index 000000000000..8ceb8355c8ff
--- /dev/null
+++ b/tests/sys/netinet/libalias/util.c
@@ -0,0 +1,123 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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 <stdio.h>
+#include <stdlib.h>
+
+#include <netinet/in.h>
+
+#include "util.h"
+
+/* 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) };
+struct in_addr cgn = { htonl(0x6440dead) };
+struct in_addr ext = { htonl(0x12345678) };
+struct in_addr ANY_ADDR = { 0 };
+
+#define REQUIRE(x) do { \
+ if (!(x)) { \
+ fprintf(stderr, "Failed in %s %s:%d.\n",\
+ __FUNCTION__, __FILE__, __LINE__); \
+ exit(-1); \
+ } \
+} while(0)
+
+int
+randcmp(const void *a, const void *b)
+{
+ int res, r = rand();
+
+ (void)a;
+ (void)b;
+ res = (r/4 < RAND_MAX/9) ? 1
+ : (r/5 < RAND_MAX/9) ? 0
+ : -1;
+ return (res);
+}
+
+void
+hexdump(void *p, size_t len)
+{
+ size_t i;
+ unsigned char *c = p;
+
+ for (i = 0; i < len; i++) {
+ printf(" %02x", c[i]);
+ switch (i & 0xf) {
+ case 0xf: printf("\n"); break;
+ case 0x7: printf(" "); break;
+ default: break;
+ }
+ }
+ if ((i & 0xf) != 0x0)
+ printf("\n");
+}
+
+struct ip *
+ip_packet(u_char protocol, size_t len)
+{
+ struct ip * p;
+
+ REQUIRE(len >= 64 && len <= IP_MAXPACKET);
+
+ p = calloc(1, len);
+ REQUIRE(p != NULL);
+
+ p->ip_v = IPVERSION;
+ p->ip_hl = sizeof(*p)/4;
+ p->ip_len = htons(len);
+ p->ip_ttl = IPDEFTTL;
+ p->ip_p = protocol;
+ REQUIRE(p->ip_hl == 5);
+
+ return (p);
+}
+
+struct udphdr *
+set_udp(struct ip *p, u_short sport, u_short dport) {
+ int hlen = p->ip_hl << 2;
+ struct udphdr *u = (struct udphdr *)((uintptr_t)p + hlen);
+ int payload = ntohs(p->ip_len) - hlen;
+
+ REQUIRE(payload >= (int)sizeof(*u));
+ p->ip_p = IPPROTO_UDP;
+ u->uh_sport = htons(sport);
+ u->uh_dport = htons(dport);
+ u->uh_ulen = htons(payload);
+ return (u);
+}
diff --git a/tests/sys/netinet/libalias/util.h b/tests/sys/netinet/libalias/util.h
new file mode 100644
index 000000000000..f58a1ad26248
--- /dev/null
+++ b/tests/sys/netinet/libalias/util.h
@@ -0,0 +1,137 @@
+/*
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright 2021 Lutz Donnerhacke
+ *
+ * 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.
+ * 3. Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT HOLDER 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/types.h>
+
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include <netinet/udp.h>
+
+#ifndef _UTIL_H
+#define _UTIL_H
+
+/* common ip ranges */
+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);
+struct ip * ip_packet(u_char protocol, size_t len);
+struct udphdr * set_udp(struct ip *p, u_short sport, u_short dport);
+
+static inline int
+addr_eq(struct in_addr a, struct in_addr b)
+{
+ return a.s_addr == b.s_addr;
+}
+
+#define a2h(a) ntohl(a.s_addr)
+
+static inline int
+rand_range(int min, int max)
+{
+ return min + rand()%(max - min);
+}
+
+#define NAT_CHECK(pip, src, dst, msq) do { \
+ int res; \
+ int len = ntohs(pip->ip_len); \
+ pip->ip_src = src; \
+ pip->ip_dst = dst; \
+ res = LibAliasOut(la, pip, len); \
+ ATF_CHECK_MSG(res == PKT_ALIAS_OK, \
+ ">%d< not met PKT_ALIAS_OK", res); \
+ ATF_CHECK(addr_eq(msq, pip->ip_src)); \
+ ATF_CHECK(addr_eq(dst, pip->ip_dst)); \
+} while(0)
+
+#define NAT_FAIL(pip, src, dst) do { \
+ int res; \
+ int len = ntohs(pip->ip_len); \
+ pip->ip_src = src; \
+ pip->ip_dst = dst; \
+ res = LibAliasOut(la, pip, len); \
+ ATF_CHECK_MSG(res != PKT_ALIAS_OK, \
+ ">%d< not met !PKT_ALIAS_OK", res); \
+ ATF_CHECK(addr_eq(src, pip->ip_src)); \
+ ATF_CHECK(addr_eq(dst, pip->ip_dst)); \
+} while(0)
+
+#define UNNAT_CHECK(pip, src, dst, rel) do { \
+ int res; \
+ int len = ntohs(pip->ip_len); \
+ pip->ip_src = src; \
+ pip->ip_dst = dst; \
+ res = LibAliasIn(la, pip, len); \
+ ATF_CHECK_MSG(res == PKT_ALIAS_OK, \
+ ">%d< not met PKT_ALIAS_OK", res); \
+ ATF_CHECK(addr_eq(src, pip->ip_src)); \
+ ATF_CHECK(addr_eq(rel, pip->ip_dst)); \
+} while(0)
+
+#define UNNAT_FAIL(pip, src, dst) do { \
+ int res; \
+ int len = ntohs(pip->ip_len); \
+ pip->ip_src = src; \
+ pip->ip_dst = dst; \
+ res = LibAliasIn(la, pip, len); \
+ ATF_CHECK_MSG(res != PKT_ALIAS_OK, \
+ ">%d< not met !PKT_ALIAS_OK", res); \
+ ATF_CHECK(addr_eq(src, pip->ip_src)); \
+ ATF_CHECK(addr_eq(dst, pip->ip_dst)); \
+} while(0)
+
+#define UDP_NAT_CHECK(p, u, si, sp, di, dp, mi) do { \
+ u = set_udp(p, (sp), (dp)); \
+ NAT_CHECK(p, (si), (di), (mi)); \
+ ATF_CHECK(u->uh_dport == htons(dp)); \
+} while(0)
+
+#define UDP_NAT_FAIL(p, u, si, sp, di, dp) do { \
+ u = set_udp(p, (sp), (dp)); \
+ NAT_FAIL(p, (si), (di)); \
+} while(0)
+
+#define UDP_UNNAT_CHECK(p, u, si, sp, mi, mp, di, dp) \
+do { \
+ u = set_udp(p, (sp), (mp)); \
+ UNNAT_CHECK(p, (si), (mi), (di)); \
+ ATF_CHECK(u->uh_sport == htons(sp)); \
+ ATF_CHECK(u->uh_dport == htons(dp)); \
+} while(0)
+
+#define UDP_UNNAT_FAIL(p, u, si, sp, mi, mp) do { \
+ u = set_udp(p, (sp), (mp)); \
+ UNNAT_FAIL(p, (si), (mi)); \
+} while(0)
+
+#endif /* _UTIL_H */
diff --git a/tests/sys/netinet/lpm.sh b/tests/sys/netinet/lpm.sh
new file mode 100755
index 000000000000..1cc377f91d9b
--- /dev/null
+++ b/tests/sys/netinet/lpm.sh
@@ -0,0 +1,178 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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
+
+setup_networking()
+{
+ jname="$1"
+ lo_dst="$2"
+ epair0="$3"
+ epair1="$4"
+
+ vnet_mkjail ${jname}a ${epair0}a ${epair1}a
+ # Setup transit IPv4 networks
+ jexec ${jname}a ifconfig ${epair0}a up
+ jexec ${jname}a ifconfig ${epair0}a inet 203.0.113.1/30
+ jexec ${jname}a ifconfig ${epair1}a up
+ jexec ${jname}a ifconfig ${epair1}a inet 203.0.113.5/30
+
+ vnet_mkjail ${jname}b ${epair0}b ${epair1}b ${lo_dst}
+ jexec ${jname}b ifconfig ${epair0}b up
+ jexec ${jname}b ifconfig ${epair0}b inet 203.0.113.2/30
+ jexec ${jname}b ifconfig ${epair1}b up
+ jexec ${jname}b ifconfig ${epair1}b inet 203.0.113.6/30
+ jexec ${jname}b ifconfig ${lo_dst} up
+
+}
+
+atf_test_case "lpm_test1_success" "cleanup"
+lpm_test1_success_head()
+{
+
+ atf_set descr 'Test IPv4 LPM for /30 and /31'
+ atf_set require.user root
+}
+
+lpm_test1_success_body()
+{
+
+ vnet_init
+
+ jname="v4t-lpm_test1_success"
+
+ lo_dst=$(vnet_mkloopback)
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+
+ setup_networking ${jname} ${lo_dst} ${epair0} ${epair1}
+
+ jexec ${jname}b ifconfig ${lo_dst} inet 198.51.100.0/32
+ jexec ${jname}b ifconfig ${lo_dst} alias 198.51.100.2/32
+
+ # Add routes
+ # A -> towards B via epair0a
+ jexec ${jname}a route add -4 -net 198.51.100.0/30 203.0.113.2
+ # A -> towards B via epair1a
+ jexec ${jname}a route add -4 -net 198.51.100.0/31 203.0.113.6
+
+ count=20
+ valid_message="${count} packets transmitted, ${count} packets received"
+
+ # Check that 198.51.100.0 goes via epair1
+ atf_check -o match:"${valid_message}" jexec ${jname}a ping -f -nc${count} 198.51.100.0
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_1} -le ${count} ]; then
+ echo "LPM failure: 1: ${pkt_0} 2: ${pkt_1} (should be ${count})"
+ exit 1
+ fi
+
+ # Check that 198.51.100.2 goes via epair0
+ atf_check -o match:"${valid_message}" jexec ${jname}a ping -f -nc${count} 198.51.100.2
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_0} -le ${count} ]; then
+ echo "LPM failure: 1: ${pkt_0} (should be ${count}) 2: ${pkt_1}"
+ exit 1
+ fi
+
+ echo "RAW BALANCING: 1: ${pkt_0} 2: ${pkt_1}"
+}
+
+lpm_test1_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "lpm_test2_success" "cleanup"
+lpm_test2_success_head()
+{
+
+ atf_set descr 'Test IPv4 LPM for the host routes'
+ atf_set require.user root
+}
+
+lpm_test2_success_body()
+{
+
+ vnet_init
+
+ jname="v4t-lpm_test2_success"
+
+ lo_dst=$(vnet_mkloopback)
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+
+ setup_networking ${jname} ${lo_dst} ${epair0} ${epair1}
+
+ jexec ${jname}b ifconfig ${lo_dst} inet 198.51.100.0/32
+ jexec ${jname}b ifconfig ${lo_dst} alias 198.51.100.1/32
+
+ # Add routes
+ # A -> towards B via epair0a
+ jexec ${jname}a route add -4 -host 198.51.100.0 203.0.113.2
+ # A -> towards B via epair1a
+ jexec ${jname}a route add -4 -host 198.51.100.1 203.0.113.6
+
+ count=20
+ valid_message="${count} packets transmitted, ${count} packets received"
+
+ # Check that 198.51.100.0 goes via epair0
+ atf_check -o match:"${valid_message}" jexec ${jname}a ping -f -nc${count} 198.51.100.0
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_0} -le ${count} ]; then
+ echo "LPM failure: 1: ${pkt_0} (should be ${count}) 2: ${pkt_1}"
+ exit 1
+ fi
+
+ # Check that 198.51.100.1 goes via epair1
+ atf_check -o match:"${valid_message}" jexec ${jname}a ping -f -nc${count} 198.51.100.1
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_1} -le ${count} ]; then
+ echo "LPM failure: 1: ${pkt_0} 2: ${pkt_1} (should be ${count})"
+ exit 1
+ fi
+
+ echo "RAW BALANCING: 1: ${pkt_0} 2: ${pkt_1}"
+}
+
+lpm_test2_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "lpm_test1_success"
+ atf_add_test_case "lpm_test2_success"
+}
+
diff --git a/tests/sys/netinet/multicast-receive.c b/tests/sys/netinet/multicast-receive.c
new file mode 100644
index 000000000000..62fc68200dd6
--- /dev/null
+++ b/tests/sys/netinet/multicast-receive.c
@@ -0,0 +1,134 @@
+/*-
+ * 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 <netinet/ip.h>
+#include <arpa/inet.h>
+#include <net/if.h>
+#include <assert.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+#include <limits.h>
+#include <err.h>
+
+static in_port_t
+atop(const char *c)
+{
+ unsigned long ul;
+
+ errno = 0;
+ if ((ul = strtol(c, NULL, 10)) < 1 || ul > IPPORT_MAX || errno != 0)
+ err(1, "can't parse %s", c);
+
+ return ((in_port_t)ul);
+}
+
+int
+main(int argc, char *argv[])
+{
+ char buf[IP_MAXPACKET + 1];
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ socklen_t slen = sizeof(struct sockaddr_in);
+ struct in_addr maddr, ifaddr;
+ ssize_t len;
+ int s, ifindex;
+ bool index;
+
+ if (argc < 4)
+usage:
+ errx(1, "Usage: %s (ip_mreq|ip_mreqn|group_req) "
+ "IPv4-group port interface", argv[0]);
+
+ if (inet_pton(AF_INET, argv[2], &maddr) != 1)
+ err(1, "inet_pton(%s) failed", argv[2]);
+ sin.sin_port = htons(atop(argv[3]));
+ if (inet_pton(AF_INET, argv[4], &ifaddr) == 1)
+ index = false;
+ else if ((ifindex = if_nametoindex(argv[4])) > 0)
+ index = true;
+ else if (strcmp(argv[4], "0") == 0) {
+ ifindex = 0;
+ index = true;
+ } else
+ err(1, "if_nametoindex(%s) failed", argv[4]);
+
+ assert((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ assert(bind(s, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+
+ if (strcmp(argv[1], "ip_mreq") == 0) {
+ if (index)
+ errx(1, "ip_mreq doesn't accept index");
+ struct ip_mreq mreq = {
+ .imr_multiaddr = maddr,
+ .imr_interface = ifaddr,
+ };
+ if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq,
+ sizeof(mreq)) != 0)
+ err(EX_OSERR, "setsockopt");
+ } else if (strcmp(argv[1], "ip_mreqn") == 0) {
+ /*
+ * ip_mreqn shall be used with index, but for testing
+ * purposes accept address too.
+ */
+ struct ip_mreqn mreqn = {
+ .imr_multiaddr = maddr,
+ .imr_address = index ? (struct in_addr){ 0 } : ifaddr,
+ .imr_ifindex = index ? ifindex : 0,
+ };
+ if (setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreqn,
+ sizeof(mreqn)) != 0)
+ err(EX_OSERR, "setsockopt");
+ } else if (strcmp(argv[1], "group_req") == 0) {
+ if (!index)
+ errx(1, "group_req expects index");
+ struct group_req greq = { .gr_interface = ifindex };
+ struct sockaddr_in *gsa = (struct sockaddr_in *)&greq.gr_group;
+
+ gsa->sin_family = AF_INET;
+ gsa->sin_len = sizeof(struct sockaddr_in);
+ gsa->sin_addr = maddr;
+ if (setsockopt(s, IPPROTO_IP, MCAST_JOIN_GROUP, &greq,
+ sizeof(greq)) != 0)
+ err(EX_OSERR, "setsockopt");
+ } else
+ goto usage;
+
+ assert((len = recvfrom(s, buf, sizeof(buf) - 1, 0,
+ (struct sockaddr *)&sin, &slen)) > 0);
+ buf[len] = '\0';
+ printf("%s:%u %s\n", inet_ntoa(sin.sin_addr), ntohs(sin.sin_port), buf);
+
+ return (0);
+}
diff --git a/tests/sys/netinet/multicast-send.c b/tests/sys/netinet/multicast-send.c
new file mode 100644
index 000000000000..f10b2b6338dd
--- /dev/null
+++ b/tests/sys/netinet/multicast-send.c
@@ -0,0 +1,97 @@
+/*-
+ * 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 <net/if.h>
+#include <assert.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <err.h>
+
+static in_port_t
+atop(const char *c)
+{
+ unsigned long ul;
+
+ errno = 0;
+ if ((ul = strtol(c, NULL, 10)) < 1 || ul > IPPORT_MAX || errno != 0)
+ err(1, "can't parse %s", c);
+
+ return ((in_port_t)ul);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct sockaddr_in src = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ }, dst = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(struct sockaddr_in),
+ };
+ struct ip_mreqn mreqn;
+ struct in_addr in;
+ int s;
+ bool index;
+
+ if (argc < 7)
+ errx(1, "Usage: %s src-IPv4 src-port dst-IPv4 dst-port "
+ "interface payload", argv[0]);
+
+ if (inet_pton(AF_INET, argv[1], &src.sin_addr) != 1)
+ err(1, "inet_pton(%s) failed", argv[1]);
+ src.sin_port = htons(atop(argv[2]));
+ if (inet_pton(AF_INET, argv[3], &dst.sin_addr) != 1)
+ err(1, "inet_pton(%s) failed", argv[3]);
+ dst.sin_port = htons(atop(argv[4]));
+ if (inet_pton(AF_INET, argv[5], &in) == 1)
+ index = false;
+ else if ((mreqn.imr_ifindex = if_nametoindex(argv[5])) > 0)
+ index = true;
+ else
+ err(1, "if_nametoindex(%s) failed", argv[5]);
+
+ assert((s = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ assert(bind(s, (struct sockaddr *)&src, sizeof(src)) == 0);
+ if (index)
+ assert(setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &mreqn,
+ sizeof(mreqn)) == 0);
+ else
+ assert(setsockopt(s, IPPROTO_IP, IP_MULTICAST_IF, &in,
+ sizeof(in)) == 0);
+ if (sendto(s, argv[6], strlen(argv[6]) + 1, 0, (struct sockaddr *)&dst,
+ sizeof(dst)) != (ssize_t)strlen(argv[6]) + 1)
+ err(1, "sendto failed");
+
+ return (0);
+}
diff --git a/tests/sys/netinet/multicast.sh b/tests/sys/netinet/multicast.sh
new file mode 100755
index 000000000000..34094ff08705
--- /dev/null
+++ b/tests/sys/netinet/multicast.sh
@@ -0,0 +1,205 @@
+#
+# 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
+
+# Set up two jails, mjail1 and mjail2, connected with two interface pairs
+multicast_vnet_init()
+{
+
+ vnet_init
+ epair1=$(vnet_mkepair)
+ epair2=$(vnet_mkepair)
+ vnet_mkjail mjail1 ${epair1}a ${epair2}a
+ jexec mjail1 ifconfig ${epair1}a up
+ jexec mjail1 ifconfig ${epair1}a 192.0.2.1/24
+ jexec mjail1 ifconfig ${epair2}a up
+ jexec mjail1 ifconfig ${epair2}a 192.0.3.1/24
+ vnet_mkjail mjail2 ${epair1}b ${epair2}b
+ jexec mjail2 ifconfig ${epair1}b up
+ jexec mjail2 ifconfig ${epair1}b 192.0.2.2/24
+ jexec mjail2 ifconfig ${epair2}b up
+ jexec mjail2 ifconfig ${epair2}b 192.0.3.2/24
+}
+
+multicast_join()
+{
+ jexec mjail2 $(atf_get_srcdir)/multicast-receive \
+ $1 233.252.0.1 6676 $2 > out & pid=$!
+ while ! jexec mjail2 ifmcstat | grep -q 233\.252\.0\.1; do
+ sleep 0.01
+ done
+}
+
+atf_test_case "IP_ADD_MEMBERSHIP_ip_mreq" "cleanup"
+IP_ADD_MEMBERSHIP_ip_mreq_head()
+{
+ atf_set descr 'IP_ADD_MEMBERSHIP / IP_MULTICAST_IF with ip_mreq'
+ atf_set require.user root
+}
+IP_ADD_MEMBERSHIP_ip_mreq_body()
+{
+ multicast_vnet_init
+
+ # join group on interface with IP address 192.0.2.2
+ multicast_join ip_mreq 192.0.2.2
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.2.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.2.1:6676 hello\n" cat out
+
+ # join group on interface with IP address 192.0.3.2
+ multicast_join ip_mreq 192.0.3.2
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.3.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+
+ # join group on the first multicast capable interface (epair1a)
+ multicast_join ip_mreq 0.0.0.0
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.2.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.2.1:6676 hello\n" cat out
+
+ # Set up the receiving jail so that first multicast capable interface
+ # is epair1a and default route points into epair2a. This will allow us
+ # to exercise both branches of inp_lookup_mcast_ifp().
+ jexec mjail2 route add default 192.0.3.254
+
+ # join group on the interface determined by the route lookup
+ multicast_join ip_mreq 0.0.0.0
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.3.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+}
+IP_ADD_MEMBERSHIP_ip_mreq_cleanup()
+{
+ rm out
+ vnet_cleanup
+}
+
+atf_test_case "IP_ADD_MEMBERSHIP_ip_mreqn" "cleanup"
+IP_ADD_MEMBERSHIP_ip_mreqn_head()
+{
+ atf_set descr 'IP_ADD_MEMBERSHIP / IP_MULTICAST_IF with ip_mreqn'
+ atf_set require.user root
+}
+IP_ADD_MEMBERSHIP_ip_mreqn_body()
+{
+ multicast_vnet_init
+
+ # join group on interface epair2
+ multicast_join ip_mreqn ${epair1}b
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 ${epair1}a hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.2.1:6676 hello\n" cat out
+
+ # join group on interface epair2
+ multicast_join ip_mreqn ${epair2}b
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 ${epair2}a hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+
+ # try to join group on the interface determined by the route lookup
+ atf_check -s exit:71 -e inline:"multicast-receive: setsockopt: Can't assign requested address\n" \
+ jexec mjail2 $(atf_get_srcdir)/multicast-receive \
+ ip_mreqn 233.252.0.1 6676 0
+ # add route and try again
+ jexec mjail2 route add default 192.0.3.254
+ multicast_join ip_mreqn 0
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.3.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+}
+IP_ADD_MEMBERSHIP_ip_mreqn_cleanup()
+{
+ rm out
+ vnet_cleanup
+}
+
+atf_test_case "MCAST_JOIN_GROUP" "cleanup"
+MCAST_JOIN_GROUP_head()
+{
+ atf_set descr 'MCAST_JOIN_GROUP'
+ atf_set require.user root
+}
+MCAST_JOIN_GROUP_body()
+{
+ multicast_vnet_init
+
+ # join group on interface epair1
+ multicast_join group_req ${epair1}b
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 ${epair1}a hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.2.1:6676 hello\n" cat out
+
+ # join group on interface epair2
+ multicast_join group_req ${epair2}b
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 ${epair2}a hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+
+ # try to join group on the interface determined by the route lookup
+ atf_check -s exit:71 -e inline:"multicast-receive: setsockopt: Can't assign requested address\n" \
+ jexec mjail2 $(atf_get_srcdir)/multicast-receive \
+ group_req 233.252.0.1 6676 0
+ # add route and try again
+ jexec mjail2 route add default 192.0.3.254
+ multicast_join group_req 0
+ atf_check -s exit:0 -o empty \
+ jexec mjail1 $(atf_get_srcdir)/multicast-send \
+ 0.0.0.0 6676 233.252.0.1 6676 192.0.3.1 hello
+ atf_check -s exit:0 sh -c "wait $pid; exit $?"
+ atf_check -s exit:0 -o inline:"192.0.3.1:6676 hello\n" cat out
+}
+MCAST_JOIN_GROUP_cleanup()
+{
+ rm out
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "IP_ADD_MEMBERSHIP_ip_mreq"
+ atf_add_test_case "IP_ADD_MEMBERSHIP_ip_mreqn"
+ atf_add_test_case "MCAST_JOIN_GROUP"
+}
diff --git a/tests/sys/netinet/output.sh b/tests/sys/netinet/output.sh
new file mode 100755
index 000000000000..23d427605878
--- /dev/null
+++ b/tests/sys/netinet/output.sh
@@ -0,0 +1,593 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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 "output_tcp_setup_success" "cleanup"
+output_tcp_setup_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 TCP output'
+ atf_set require.user root
+}
+
+output_tcp_setup_success_body()
+{
+
+ vnet_init
+
+ net_src="192.0.2."
+ net_dst="192.0.2."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}2"
+ plen=24
+ text="testtesttst"
+ port=4242
+
+ script_name=`dirname $0`/../common/net_receiver.py
+ script_name=`realpath ${script_name}`
+ jname="v4t-output_tcp_setup_success"
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail ${jname}a ${epair}a
+ jexec ${jname}a ifconfig ${epair}a up
+ jexec ${jname}a ifconfig ${epair}a inet ${ip_src}/${plen}
+
+ vnet_mkjail ${jname}b ${epair}b
+ jexec ${jname}b ifconfig ${epair}b up
+
+ jexec ${jname}b ifconfig ${epair}b inet ${ip_dst}/${plen}
+
+ # run listener
+ args="--family inet --ports ${port} --match_str ${text}"
+ echo jexec ${jname}b ${script_name} ${args}
+ jexec ${jname}b ${script_name} --test_name "test_listen_tcp" ${args} &
+ cmd_pid=$!
+
+ # wait for the script init
+ counter=0
+ while [ `jexec ${jname}b sockstat -4qlp ${port} | wc -l` != "1" ]; do
+ sleep 0.01
+ counter=$((counter+1))
+ if [ ${counter} -ge 50 ]; then break; fi
+ done
+ if [ `jexec ${jname}b sockstat -4qlp ${port} | wc -l` != "1" ]; then
+ echo "App setup failed"
+ exit 1
+ fi
+
+ # run sender
+ echo -n "${text}" | jexec ${jname}a nc -N ${ip_dst} ${port}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "sender exit code $exit_code" ; fi
+
+ wait ${cmd_pid}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "receiver exit code $exit_code" ; fi
+}
+
+output_tcp_setup_success_cleanup()
+{
+ vnet_cleanup
+}
+
+
+atf_test_case "output_udp_setup_success" "cleanup"
+output_udp_setup_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 UDP output'
+ atf_set require.user root
+}
+
+output_udp_setup_success_body()
+{
+
+ vnet_init
+
+ net_src="192.0.2."
+ net_dst="192.0.2."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}2"
+ plen=24
+ text="testtesttst"
+ port=4242
+
+ script_name=`dirname $0`/../common/net_receiver.py
+ script_name=`realpath ${script_name}`
+ jname="v4t-output_udp_setup_success"
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail ${jname}a ${epair}a
+ jexec ${jname}a ifconfig ${epair}a up
+ jexec ${jname}a ifconfig ${epair}a inet ${ip_src}/${plen}
+
+ vnet_mkjail ${jname}b ${epair}b
+ jexec ${jname}b ifconfig ${epair}b up
+ jexec ${jname}b ifconfig ${epair}b inet ${ip_dst}/${plen}
+
+ # run listener
+ args="--family inet --ports ${port} --match_str ${text}"
+ echo jexec ${jname}b ${script_name} ${args}
+ jexec ${jname}b ${script_name} --test_name "test_listen_udp" ${args} &
+ cmd_pid=$!
+
+ # wait for the script init
+ counter=0
+ while [ `jexec ${jname}b sockstat -4qlp ${port} | wc -l` != "1" ]; do
+ sleep 0.1
+ counterc=$((counter+1))
+ if [ ${counter} -ge 50 ]; then break; fi
+ done
+ if [ `jexec ${jname}b sockstat -4qlp ${port} | wc -l` != "1" ]; then
+ echo "App setup failed"
+ exit 1
+ fi
+
+ # run sender
+ # TODO: switch from nc to some alternative to avoid 1-second delay
+ echo -n "${text}" | jexec ${jname}a nc -uNw1 ${ip_dst} ${port}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "sender exit code $exit_code" ; fi
+
+ wait ${cmd_pid}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "receiver exit code $exit_code" ; fi
+}
+
+output_udp_setup_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "output_raw_success" "cleanup"
+output_raw_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 raw output'
+ atf_set require.user root
+}
+
+output_raw_success_body()
+{
+
+ vnet_init
+
+ net_src="192.0.2."
+ net_dst="192.0.2."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}2"
+ plen=24
+
+ script_name=`dirname $0`/../common/net_receiver.py
+ script_name=`realpath ${script_name}`
+ jname="v4t-output_raw_success"
+
+ epair=$(vnet_mkepair)
+
+ vnet_mkjail ${jname}a ${epair}a
+ jexec ${jname}a ifconfig ${epair}a up
+ jexec ${jname}a ifconfig ${epair}a inet ${ip_src}/${plen}
+
+ vnet_mkjail ${jname}b ${epair}b
+ jexec ${jname}b ifconfig ${epair}b up
+
+ jexec ${jname}b ifconfig ${epair}b inet ${ip_dst}/${plen}
+
+ atf_check -o match:'1 packets transmitted, 1 packets received' jexec ${jname}a ping -nc1 ${ip_dst}
+}
+
+output_raw_success_cleanup()
+{
+ vnet_cleanup
+}
+
+# Multipath tests are done the following way:
+# epair0
+# jailA lo < > lo jailB
+# epair1
+# jailA has 2 routes towards /24 prefix on jailB loopback, via 2 epairs
+# jailB has 1 route towards /24 prefix on jailA loopback, via epair0
+#
+# jailA initiates connections/sends packets towards IPs on jailB loopback.
+# Script then compares amount of packets sent via epair0 and epair1
+
+mpath_check()
+{
+ if [ `sysctl -iW net.route.multipath | wc -l` != "1" ]; then
+ atf_skip "This test requires ROUTE_MPATH enabled"
+ fi
+}
+
+mpath_enable()
+{
+ jexec $1 sysctl net.route.multipath=1
+ if [ $? != 0 ]; then
+ atf_fail "Setting multipath in jail $1 failed".
+ fi
+}
+
+atf_test_case "output_tcp_flowid_mpath_success" "cleanup"
+output_tcp_flowid_mpath_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 TCP output flowid generation'
+ atf_set require.user root
+}
+
+output_tcp_flowid_mpath_success_body()
+{
+ vnet_init
+ mpath_check
+
+ net_src="192.0.2."
+ net_dst="198.51.100."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}1"
+ plen=24
+ text="testtesttst"
+
+ script_name=`dirname $0`/../common/net_receiver.py
+ script_name=`realpath ${script_name}`
+ jname="v4t-output_tcp_flowid_mpath_success"
+
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+ lo_src=$(vnet_mkloopback)
+ lo_dst=$(vnet_mkloopback)
+
+ vnet_mkjail ${jname}a ${epair0}a ${epair1}a ${lo_src}
+ mpath_enable ${jname}a
+ # Setup transit IPv4 networks
+ jexec ${jname}a ifconfig ${epair0}a up
+ jexec ${jname}a ifconfig ${epair0}a inet 203.0.113.1/30
+ jexec ${jname}a ifconfig ${epair1}a up
+ jexec ${jname}a ifconfig ${epair1}a inet 203.0.113.5/30
+ jexec ${jname}a ifconfig ${lo_src} up
+
+ vnet_mkjail ${jname}b ${epair0}b ${epair1}b ${lo_dst}
+ jexec ${jname}b ifconfig ${epair0}b up
+ jexec ${jname}b ifconfig ${epair0}b inet 203.0.113.2/30
+ jexec ${jname}b ifconfig ${epair1}b up
+ jexec ${jname}b ifconfig ${epair1}b inet 203.0.113.6/30
+ jexec ${jname}b ifconfig ${lo_dst} up
+
+ # DST ips/ports to test
+ ips="4 29 48 53 55 61 71 80 84 87 90 91 119 131 137 153 154 158 162 169 169 171 176 187 197 228 233 235 236 237 245 251"
+ ports="53540 49743 43067 9131 16734 5150 14379 40292 20634 51302 3387 24387 9282 14275 42103 26881 42461 29520 45714 11096"
+
+ jexec ${jname}a ifconfig ${lo_src} inet ${ip_src}/32
+
+ jexec ${jname}b ifconfig ${lo_dst} inet ${ip_dst}/32
+ for i in ${ips}; do
+ jexec ${jname}b ifconfig ${lo_dst} alias ${net_dst}${i}/32
+ done
+
+ # Add routes
+ # A -> towards B via epair0a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.2
+ # A -> towards B via epair1a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.6
+
+ # B towards A via epair0b
+ jexec ${jname}b route add -4 -net ${net_src}0/${plen} 203.0.113.1
+
+ # Base setup verification
+ atf_check -o match:'1 packets transmitted, 1 packets received' jexec ${jname}a ping -nc1 ${ip_dst}
+
+ # run listener
+ num_ports=`echo ${ports} | wc -w`
+ num_ips=`echo ${ips} | wc -w`
+ count_examples=$((num_ports*num_ips))
+ listener_ports=`echo ${ports} | tr ' ' '\n' | sort -n | tr '\n' ',' | sed -e 's?,$??'`
+ args="--family inet --ports ${listener_ports} --count ${count_examples} --match_str ${text}"
+ echo jexec ${jname}b ${script_name} ${args}
+ jexec ${jname}b ${script_name} --test_name "test_listen_tcp" ${args} &
+ cmd_pid=$!
+
+ # wait for the app init
+ counter=0
+ init=0
+ while [ ${counter} -le 50 ]; do
+ _ports=`jexec ${jname}b sockstat -4ql | awk "\\\$3 == ${cmd_pid} {print \\\$6}"|awk -F: "{print \\\$2}" | sort -n | tr '\n' ','`
+ if [ "${_ports}" = "${listener_ports}," ]; then
+ init=1
+ break;
+ fi
+ done
+ if [ ${init} -eq 0 ]; then
+ jexec ${jname}b sockstat -6ql | awk "\$3 == ${cmd_pid}"
+ echo "App setup failed"
+ exit 1
+ fi
+ echo "App setup done"
+
+ # run sender
+ for _ip in ${ips}; do
+ ip="${net_dst}${_ip}"
+ for port in ${ports}; do
+ echo -n "${text}" | jexec ${jname}a nc -nN ${ip} ${port}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "sender exit code $exit_code" ; fi
+ done
+ done
+
+ wait ${cmd_pid}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "receiver exit code $exit_code" ; fi
+
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_0} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ fi
+ if [ ${pkt_1} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ exit 1
+ fi
+ echo "TCP Balancing: 1: ${pkt_0} 2: ${pkt_1}"
+}
+
+output_tcp_flowid_mpath_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "output_udp_flowid_mpath_success" "cleanup"
+output_udp_flowid_mpath_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 UDP output flowid generation'
+ atf_set require.user root
+}
+
+output_udp_flowid_mpath_success_body()
+{
+
+ vnet_init
+ mpath_check
+
+ # Note this test will spawn around ~100 nc processes
+
+ net_src="192.0.2."
+ net_dst="198.51.100."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}1"
+ plen=24
+ text="testtesttst"
+
+ script_name=`dirname $0`/../common/net_receiver.py
+ script_name=`realpath ${script_name}`
+ jname="v4t-output_udp_flowid_mpath_success"
+
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+ lo_src=$(vnet_mkloopback)
+ lo_dst=$(vnet_mkloopback)
+
+ vnet_mkjail ${jname}a ${epair0}a ${epair1}a ${lo_src}
+ mpath_enable ${jname}a
+ # Setup transit IPv4 networks
+ jexec ${jname}a ifconfig ${epair0}a up
+ jexec ${jname}a ifconfig ${epair0}a inet 203.0.113.1/30
+ jexec ${jname}a ifconfig ${epair1}a up
+ jexec ${jname}a ifconfig ${epair1}a inet 203.0.113.5/30
+ jexec ${jname}a ifconfig ${lo_src} up
+
+ vnet_mkjail ${jname}b ${epair0}b ${epair1}b ${lo_dst}
+ jexec ${jname}b ifconfig ${epair0}b up
+ jexec ${jname}b ifconfig ${epair0}b inet 203.0.113.2/30
+ jexec ${jname}b ifconfig ${epair1}b up
+ jexec ${jname}b ifconfig ${epair1}b inet 203.0.113.6/30
+ jexec ${jname}b ifconfig ${lo_dst} up
+
+ # DST ips/ports to test
+ ips="4 29 48 53 55 61 71 80 84 87 90 91 119 131 137 153 154 158 162 169 169 171 176 187 197 228 233 235 236 237 245 251"
+ ports="53540 49743 43067 9131 16734 5150 14379 40292 20634 51302 3387 24387 9282 14275 42103 26881 42461 29520 45714 11096"
+
+ jexec ${jname}a ifconfig ${lo_src} inet ${ip_src}/32
+
+ jexec ${jname}b ifconfig ${lo_dst} inet ${ip_dst}/32
+ for i in ${ips}; do
+ jexec ${jname}b ifconfig ${lo_dst} alias ${net_dst}${i}/32
+ done
+
+ # Add routes
+ # A -> towards B via epair0a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.2
+ # A -> towards B via epair1a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.6
+
+ # B towards A via epair0b
+ jexec ${jname}b route add -4 -net ${net_src}0/${plen} 203.0.113.1
+
+ # Base setup verification
+ atf_check -o match:'1 packets transmitted, 1 packets received' jexec ${jname}a ping -nc1 ${ip_dst}
+
+ # run listener
+ num_ports=`echo ${ports} | wc -w`
+ num_ips=`echo ${ips} | wc -w`
+ count_examples=$((num_ports*num_ips))
+ listener_ports=`echo ${ports} | tr ' ' '\n' | sort -n | tr '\n' ',' | sed -e 's?,$??'`
+ args="--family inet --ports ${listener_ports} --count ${count_examples} --match_str ${text}"
+ echo jexec ${jname}b ${script_name} ${args}
+ jexec ${jname}b ${script_name} --test_name "test_listen_udp" ${args} &
+ cmd_pid=$!
+
+ # wait for the app init
+ counter=0
+ init=0
+ while [ ${counter} -le 50 ]; do
+ _ports=`jexec ${jname}b sockstat -4ql | awk "\\\$3 == ${cmd_pid} {print \\\$6}"|awk -F: "{print \\\$2}" | sort -n | tr '\n' ','`
+ if [ "${_ports}" = "${listener_ports}," ]; then
+ init=1
+ break;
+ fi
+ done
+ if [ ${init} -eq 0 ]; then
+ jexec ${jname}b sockstat -4ql | awk "\$3 == ${cmd_pid}"
+ echo "App setup failed"
+ exit 1
+ fi
+ echo "App setup done"
+
+ # run sender
+ for _ip in ${ips}; do
+ ip="${net_dst}${_ip}"
+ for port in ${ports}; do
+ # XXX: switch to something that allows immediate exit
+ echo -n "${text}" | jexec ${jname}a nc -nuNw1 ${ip} ${port} &
+ sleep 0.01
+ done
+ done
+
+ wait ${cmd_pid}
+ exit_code=$?
+ if [ $exit_code -ne 0 ]; then atf_fail "receiver exit code $exit_code" ; fi
+
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+ if [ ${pkt_0} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ fi
+ if [ ${pkt_1} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ fi
+ echo "UDP BALANCING: 1: ${pkt_0} 2: ${pkt_1}"
+}
+
+output_udp_flowid_mpath_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "output_raw_flowid_mpath_success" "cleanup"
+output_raw_flowid_mpath_success_head()
+{
+
+ atf_set descr 'Test valid IPv4 raw output flowid generation'
+ atf_set require.user root
+}
+
+output_raw_flowid_mpath_success_body()
+{
+
+ vnet_init
+ mpath_check
+
+ net_src="192.0.2."
+ net_dst="198.51.100."
+ ip_src="${net_src}1"
+ ip_dst="${net_dst}1"
+ plen=24
+ text="testtesttst"
+
+ jname="v4t-output_raw_flowid_mpath_success"
+
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+ lo_src=$(vnet_mkloopback)
+ lo_dst=$(vnet_mkloopback)
+
+ vnet_mkjail ${jname}a ${epair0}a ${epair1}a ${lo_src}
+ mpath_enable ${jname}a
+ # Setup transit IPv4 networks
+ jexec ${jname}a ifconfig ${epair0}a up
+ jexec ${jname}a ifconfig ${epair0}a inet 203.0.113.1/30
+ jexec ${jname}a ifconfig ${epair1}a up
+ jexec ${jname}a ifconfig ${epair1}a inet 203.0.113.5/30
+ jexec ${jname}a ifconfig ${lo_src} up
+
+ vnet_mkjail ${jname}b ${epair0}b ${epair1}b ${lo_dst}
+ jexec ${jname}b ifconfig ${epair0}b up
+ jexec ${jname}b ifconfig ${epair0}b inet 203.0.113.2/30
+ jexec ${jname}b ifconfig ${epair1}b up
+ jexec ${jname}b ifconfig ${epair1}b inet 203.0.113.6/30
+ jexec ${jname}b ifconfig ${lo_dst} up
+
+ # DST ips/ports to test
+ ips="4 29 48 53 55 61 71 80 84 87 90 91 119 131 137 153 154 158 162 169 169 171 176 187 197 228 233 235 236 237 245 251"
+
+ jexec ${jname}a ifconfig ${lo_src} inet ${ip_src}/32
+
+ jexec ${jname}b ifconfig ${lo_dst} inet ${ip_dst}/32
+ for i in ${ips}; do
+ jexec ${jname}b ifconfig ${lo_dst} alias ${net_dst}${i}/32
+ done
+
+ # Add routes
+ # A -> towards B via epair0a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.2
+ # A -> towards B via epair1a
+ jexec ${jname}a route add -4 -net ${net_dst}0/${plen} 203.0.113.6
+
+ # B towards A via epair0b
+ jexec ${jname}b route add -4 -net ${net_src}0/${plen} 203.0.113.1
+
+ # Base setup verification
+ atf_check -o match:'1 packets transmitted, 1 packets received' jexec ${jname}a ping -nc1 ${ip_dst}
+
+ # run sender
+ valid_message='1 packets transmitted, 1 packets received'
+ for _ip in ${ips}; do
+ ip="${net_dst}${_ip}"
+ atf_check -o match:"${valid_message}" jexec ${jname}a ping -nc1 ${ip}
+ done
+
+ pkt_0=`jexec ${jname}a netstat -Wf link -I ${epair0}a | head | awk '$1!~/^Name/{print$8}'`
+ pkt_1=`jexec ${jname}a netstat -Wf link -I ${epair1}a | head | awk '$1!~/^Name/{print$8}'`
+
+ jexec ${jname}a netstat -bWf link -I ${epair0}a
+ jexec ${jname}a netstat -bWf link -I ${epair1}a
+ if [ ${pkt_0} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ fi
+ if [ ${pkt_1} -le 10 ]; then
+ atf_fail "Balancing failure: 1: ${pkt_0} 2: ${pkt_1}"
+ fi
+ echo "RAW BALANCING: 1: ${pkt_0} 2: ${pkt_1}"
+}
+
+output_raw_flowid_mpath_success_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "output_tcp_setup_success"
+ atf_add_test_case "output_udp_setup_success"
+ atf_add_test_case "output_raw_success"
+ atf_add_test_case "output_tcp_flowid_mpath_success"
+ atf_add_test_case "output_udp_flowid_mpath_success"
+ atf_add_test_case "output_raw_flowid_mpath_success"
+}
+
diff --git a/tests/sys/netinet/redirect.py b/tests/sys/netinet/redirect.py
new file mode 100755
index 000000000000..2085d72fde2b
--- /dev/null
+++ b/tests/sys/netinet/redirect.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# -
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+#
+
+import argparse
+import logging
+logging.getLogger("scapy").setLevel(logging.CRITICAL)
+import scapy.all as sc
+import socket
+import sys
+import fcntl
+import struct
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description='ICMP redirect generator')
+ parser.add_argument('--smac', type=str, required=True,
+ help='eth source mac')
+ parser.add_argument('--dmac', type=str, required=True,
+ help='eth dest mac')
+ parser.add_argument('--sip', type=str, required=True,
+ help='remote router source ip')
+ parser.add_argument('--dip', type=str, required=True,
+ help='local router ip')
+ parser.add_argument('--iface', type=str, required=True,
+ help='ifname to send packet to')
+ parser.add_argument('--route', type=str, required=True,
+ help='destination IP to redirect')
+ parser.add_argument('--gw', type=str, required=True,
+ help='redirect GW')
+ return parser.parse_args()
+
+
+def construct_icmp_redirect(smac, dmac, sip, dip, route_dst, route_gw):
+ e = sc.Ether(src=smac, dst=dmac)
+ l3 = sc.IP(src=sip, dst=dip)
+ icmp = sc.ICMP(type=5, code=1, gw=route_gw)
+ orig_ip = sc.IP(src=sip, dst=route_dst)
+ return e / l3 / icmp / orig_ip / sc.UDP()
+
+
+def send_packet(pkt, iface, feedback=False):
+ if feedback:
+ # Make kernel receive the packet as well
+ BIOCFEEDBACK = 0x8004427c
+ socket = sc.conf.L2socket(iface=args.iface)
+ fcntl.ioctl(socket.ins, BIOCFEEDBACK, struct.pack('I', 1))
+ sc.sendp(pkt, socket=socket, verbose=True)
+ else:
+ sc.sendp(pkt, iface=iface, verbose=False)
+
+
+def main():
+ args = parse_args()
+ pkt = construct_icmp_redirect(args.smac, args.dmac, args.sip, args.dip,
+ args.route, args.gw)
+ send_packet(pkt, args.iface)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tests/sys/netinet/redirect.sh b/tests/sys/netinet/redirect.sh
new file mode 100755
index 000000000000..ad5b562da57a
--- /dev/null
+++ b/tests/sys/netinet/redirect.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2020 Alexander V. Chernikov
+#
+# 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 "valid_redirect" "cleanup"
+valid_redirect_head() {
+
+ atf_set descr 'Test valid IPv4 redirect'
+ atf_set require.user root
+ atf_set require.progs python3 scapy
+}
+
+valid_redirect_body() {
+
+ ids=65533
+ id=`printf "%x" ${ids}`
+ if [ $$ -gt 65535 ]; then
+ xl=`printf "%x" $(($$ - 65535))`
+ yl="1"
+ else
+ xl=`printf "%x" $$`
+ yl=""
+ fi
+
+ vnet_init
+
+ ip4a="192.0.2.1"
+ ip4b="192.0.2.2"
+
+ net4="198.51.100.0/24"
+ dst_addr4="198.51.100.42"
+
+ # remote_rtr
+ remote_rtr_ip="192.0.2.3"
+ remote_rtr_mac="00:00:5E:00:53:42"
+
+ new_rtr_ip="192.0.2.4"
+
+ script_name="redirect.py"
+
+ epair=$(vnet_mkepair)
+ ifconfig ${epair}a up
+ ifconfig ${epair}a inet ${ip4a}/24
+
+ jname="v4t-${id}-${yl}-${xl}"
+ vnet_mkjail ${jname} ${epair}b
+ jexec ${jname} ifconfig ${epair}b up
+ jexec ${jname} ifconfig ${epair}b inet ${ip4b}/24
+
+ # Setup static entry for the remote router
+ jexec ${jname} arp -s ${remote_rtr_ip} ${remote_rtr_mac}
+ # setup prefix reachable via router
+ jexec ${jname} route add -4 -net ${net4} ${remote_rtr_ip}
+
+ local_ip=${ip4b}
+ local_mac=`jexec ${jname} ifconfig ${epair}b ether | awk '$1~/ether/{print$2}'`
+
+ # echo "LOCAL: ${local_ip} ${local_mac}"
+ # echo "REMOTE: ${remote_rtr_ip} ${remote_rtr_mac}"
+
+ atf_check -s exit:0 $(atf_get_srcdir)/${script_name} \
+ --smac ${remote_rtr_mac} --dmac ${local_mac} \
+ --sip ${remote_rtr_ip} --dip ${local_ip} \
+ --route ${dst_addr4} --gw ${new_rtr_ip} \
+ --iface ${epair}a
+
+ atf_check -o match:"destination: ${dst_addr4}\$" jexec ${jname} route -n get -4 ${dst_addr4}
+ atf_check -o match:'flags: <UP,GATEWAY,HOST,DYNAMIC,DONE>' jexec ${jname} route -n get -4 ${dst_addr4}
+}
+
+valid_redirect_cleanup() {
+
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+
+ atf_add_test_case "valid_redirect"
+}
+
+# end
+
diff --git a/tests/sys/netinet/so_reuseport_lb_test.c b/tests/sys/netinet/so_reuseport_lb_test.c
new file mode 100644
index 000000000000..393a626af5a4
--- /dev/null
+++ b/tests/sys/netinet/so_reuseport_lb_test.c
@@ -0,0 +1,719 @@
+/*-
+ * Copyright (c) 2018 The FreeBSD Foundation
+ *
+ * This software was developed by Mark Johnston 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/event.h>
+#include <sys/filio.h>
+#include <sys/ioccom.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>
+
+#include <atf-c.h>
+
+/*
+ * Given an array of non-blocking listening sockets configured in a LB group
+ * for "addr", try connecting to "addr" in a loop and verify that connections
+ * are roughly balanced across the sockets.
+ */
+static void
+lb_simple_accept_loop(int domain, const struct sockaddr *addr, int sds[],
+ size_t nsds, int nconns)
+{
+ size_t i;
+ int *acceptcnt;
+ int csd, error, excnt, sd;
+ const struct linger lopt = { 1, 0 };
+
+ /*
+ * We expect each listening socket to accept roughly nconns/nsds
+ * connections, but allow for some error.
+ */
+ excnt = nconns / nsds / 8;
+ acceptcnt = calloc(nsds, sizeof(*acceptcnt));
+ ATF_REQUIRE_MSG(acceptcnt != NULL, "calloc() failed: %s",
+ strerror(errno));
+
+ while (nconns-- > 0) {
+ sd = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(sd >= 0, "socket() failed: %s",
+ strerror(errno));
+
+ error = connect(sd, addr, addr->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "connect() failed: %s",
+ strerror(errno));
+
+ error = setsockopt(sd, SOL_SOCKET, SO_LINGER, &lopt, sizeof(lopt));
+ ATF_REQUIRE_MSG(error == 0, "Setting linger failed: %s",
+ strerror(errno));
+
+ /*
+ * Poll the listening sockets.
+ */
+ do {
+ for (i = 0; i < nsds; i++) {
+ csd = accept(sds[i], NULL, NULL);
+ if (csd < 0) {
+ ATF_REQUIRE_MSG(errno == EWOULDBLOCK ||
+ errno == EAGAIN,
+ "accept() failed: %s",
+ strerror(errno));
+ continue;
+ }
+
+ error = close(csd);
+ ATF_REQUIRE_MSG(error == 0,
+ "close() failed: %s", strerror(errno));
+
+ acceptcnt[i]++;
+ break;
+ }
+ } while (i == nsds);
+
+ error = close(sd);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s",
+ strerror(errno));
+ }
+
+ for (i = 0; i < nsds; i++)
+ ATF_REQUIRE_MSG(acceptcnt[i] > excnt, "uneven balancing");
+}
+
+static int
+lb_listen_socket(int domain, int flags)
+{
+ int one;
+ int error, sd;
+
+ sd = socket(domain, SOCK_STREAM | flags, 0);
+ ATF_REQUIRE_MSG(sd >= 0, "socket() failed: %s", strerror(errno));
+
+ one = 1;
+ error = setsockopt(sd, SOL_SOCKET, SO_REUSEPORT_LB, &one, sizeof(one));
+ ATF_REQUIRE_MSG(error == 0, "setsockopt(SO_REUSEPORT_LB) failed: %s",
+ strerror(errno));
+
+ return (sd);
+}
+
+ATF_TC_WITHOUT_HEAD(basic_ipv4);
+ATF_TC_BODY(basic_ipv4, tc)
+{
+ struct sockaddr_in addr;
+ socklen_t slen;
+ size_t i;
+ const int nconns = 16384;
+ int error, sds[16];
+ uint16_t port;
+
+ sds[0] = lb_listen_socket(PF_INET, SOCK_NONBLOCK);
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_len = sizeof(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(0);
+ addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ error = bind(sds[0], (const struct sockaddr *)&addr, sizeof(addr));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+ error = listen(sds[0], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ slen = sizeof(addr);
+ error = getsockname(sds[0], (struct sockaddr *)&addr, &slen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(slen == sizeof(addr), "sockaddr size changed");
+ port = addr.sin_port;
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_len = sizeof(addr);
+ addr.sin_family = AF_INET;
+ addr.sin_port = port;
+ addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ for (i = 1; i < nitems(sds); i++) {
+ sds[i] = lb_listen_socket(PF_INET, SOCK_NONBLOCK);
+
+ error = bind(sds[i], (const struct sockaddr *)&addr,
+ sizeof(addr));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s",
+ strerror(errno));
+ error = listen(sds[i], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s",
+ strerror(errno));
+ }
+
+ lb_simple_accept_loop(PF_INET, (struct sockaddr *)&addr, sds,
+ nitems(sds), nconns);
+ for (i = 0; i < nitems(sds); i++) {
+ error = close(sds[i]);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s",
+ strerror(errno));
+ }
+}
+
+ATF_TC_WITHOUT_HEAD(basic_ipv6);
+ATF_TC_BODY(basic_ipv6, tc)
+{
+ const struct in6_addr loopback6 = IN6ADDR_LOOPBACK_INIT;
+ struct sockaddr_in6 addr;
+ socklen_t slen;
+ size_t i;
+ const int nconns = 16384;
+ int error, sds[16];
+ uint16_t port;
+
+ sds[0] = lb_listen_socket(PF_INET6, SOCK_NONBLOCK);
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin6_len = sizeof(addr);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = htons(0);
+ addr.sin6_addr = loopback6;
+ error = bind(sds[0], (const struct sockaddr *)&addr, sizeof(addr));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+ error = listen(sds[0], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ slen = sizeof(addr);
+ error = getsockname(sds[0], (struct sockaddr *)&addr, &slen);
+ ATF_REQUIRE_MSG(error == 0, "getsockname() failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(slen == sizeof(addr), "sockaddr size changed");
+ port = addr.sin6_port;
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin6_len = sizeof(addr);
+ addr.sin6_family = AF_INET6;
+ addr.sin6_port = port;
+ addr.sin6_addr = loopback6;
+ for (i = 1; i < nitems(sds); i++) {
+ sds[i] = lb_listen_socket(PF_INET6, SOCK_NONBLOCK);
+
+ error = bind(sds[i], (const struct sockaddr *)&addr,
+ sizeof(addr));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s",
+ strerror(errno));
+ error = listen(sds[i], 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s",
+ strerror(errno));
+ }
+
+ lb_simple_accept_loop(PF_INET6, (struct sockaddr *)&addr, sds,
+ nitems(sds), nconns);
+ for (i = 0; i < nitems(sds); i++) {
+ error = close(sds[i]);
+ ATF_REQUIRE_MSG(error == 0, "close() failed: %s",
+ strerror(errno));
+ }
+}
+
+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);
+ }
+
+ for (size_t j = nitems(threads); j > 0; j--) {
+ ATF_REQUIRE(pthread_cancel(threads[j - 1]) == 0);
+ ATF_REQUIRE(pthread_join(threads[j - 1], NULL) == 0);
+ }
+}
+
+/*
+ * 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);
+}
+
+/*
+ * The kernel erroneously permits calling connect() on a UDP socket with
+ * SO_REUSEPORT_LB set. Verify that packets sent to the bound address are
+ * dropped unless they come from the connected address.
+ */
+ATF_TC_WITHOUT_HEAD(connect_udp);
+ATF_TC_BODY(connect_udp, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(sin),
+ .sin_addr = { htonl(INADDR_LOOPBACK) },
+ };
+ ssize_t n;
+ int error, len, s1, s2, s3;
+ char ch;
+
+ s1 = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s1 >= 0);
+ s2 = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s2 >= 0);
+ s3 = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s3 >= 0);
+
+ error = setsockopt(s1, SOL_SOCKET, SO_REUSEPORT_LB, (int[]){1},
+ sizeof(int));
+ ATF_REQUIRE_MSG(error == 0,
+ "setsockopt(SO_REUSEPORT_LB) failed: %s", strerror(errno));
+ error = bind(s1, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = bind(s2, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = bind(s3, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ /* Connect to an address not owned by s2. */
+ error = getsockname(s3, (struct sockaddr *)&sin,
+ (socklen_t[]){sizeof(sin)});
+ ATF_REQUIRE(error == 0);
+ error = connect(s1, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_REQUIRE_MSG(error == 0, "connect() failed: %s", strerror(errno));
+
+ /* Try to send a packet to s1 from s2. */
+ error = getsockname(s1, (struct sockaddr *)&sin,
+ (socklen_t[]){sizeof(sin)});
+ ATF_REQUIRE(error == 0);
+
+ ch = 42;
+ n = sendto(s2, &ch, sizeof(ch), 0, (struct sockaddr *)&sin,
+ sizeof(sin));
+ ATF_REQUIRE(n == 1);
+
+ /* Give the packet some time to arrive. */
+ usleep(100000);
+
+ /* s1 is connected to s3 and shouldn't receive from s2. */
+ error = ioctl(s1, FIONREAD, &len);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE_MSG(len == 0, "unexpected data available");
+
+ /* ... but s3 can of course send to s1. */
+ n = sendto(s3, &ch, sizeof(ch), 0, (struct sockaddr *)&sin,
+ sizeof(sin));
+ ATF_REQUIRE(n == 1);
+ usleep(100000);
+ error = ioctl(s1, FIONREAD, &len);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE_MSG(len == 1, "expected data available");
+}
+
+/*
+ * The kernel erroneously permits calling connect() on a UDP socket with
+ * SO_REUSEPORT_LB set. Verify that packets sent to the bound address are
+ * dropped unless they come from the connected address.
+ */
+ATF_TC_WITHOUT_HEAD(connect_udp6);
+ATF_TC_BODY(connect_udp6, tc)
+{
+ struct sockaddr_in6 sin6 = {
+ .sin6_family = AF_INET6,
+ .sin6_len = sizeof(sin6),
+ .sin6_addr = IN6ADDR_LOOPBACK_INIT,
+ };
+ ssize_t n;
+ int error, len, s1, s2, s3;
+ char ch;
+
+ s1 = socket(PF_INET6, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s1 >= 0);
+ s2 = socket(PF_INET6, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s2 >= 0);
+ s3 = socket(PF_INET6, SOCK_DGRAM, 0);
+ ATF_REQUIRE(s3 >= 0);
+
+ error = setsockopt(s1, SOL_SOCKET, SO_REUSEPORT_LB, (int[]){1},
+ sizeof(int));
+ ATF_REQUIRE_MSG(error == 0,
+ "setsockopt(SO_REUSEPORT_LB) failed: %s", strerror(errno));
+ error = bind(s1, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = bind(s2, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ error = bind(s3, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+
+ /* Connect to an address not owned by s2. */
+ error = getsockname(s3, (struct sockaddr *)&sin6,
+ (socklen_t[]){sizeof(sin6)});
+ ATF_REQUIRE(error == 0);
+ error = connect(s1, (struct sockaddr *)&sin6, sizeof(sin6));
+ ATF_REQUIRE_MSG(error == 0, "connect() failed: %s", strerror(errno));
+
+ /* Try to send a packet to s1 from s2. */
+ error = getsockname(s1, (struct sockaddr *)&sin6,
+ (socklen_t[]){sizeof(sin6)});
+ ATF_REQUIRE(error == 0);
+
+ ch = 42;
+ n = sendto(s2, &ch, sizeof(ch), 0, (struct sockaddr *)&sin6,
+ sizeof(sin6));
+ ATF_REQUIRE(n == 1);
+
+ /* Give the packet some time to arrive. */
+ usleep(100000);
+
+ /* s1 is connected to s3 and shouldn't receive from s2. */
+ error = ioctl(s1, FIONREAD, &len);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE_MSG(len == 0, "unexpected data available");
+
+ /* ... but s3 can of course send to s1. */
+ n = sendto(s3, &ch, sizeof(ch), 0, (struct sockaddr *)&sin6,
+ sizeof(sin6));
+ ATF_REQUIRE(n == 1);
+ usleep(100000);
+ error = ioctl(s1, FIONREAD, &len);
+ ATF_REQUIRE(error == 0);
+ ATF_REQUIRE_MSG(len == 1, "expected data available");
+}
+
+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);
+ ATF_TP_ADD_TC(tp, connect_udp);
+ ATF_TP_ADD_TC(tp, connect_udp6);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/socket_afinet.c b/tests/sys/netinet/socket_afinet.c
new file mode 100644
index 000000000000..9c718fc5a901
--- /dev/null
+++ b/tests/sys/netinet/socket_afinet.c
@@ -0,0 +1,599 @@
+/*-
+ * 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
+ * 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 <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>
+
+ATF_TC_WITHOUT_HEAD(socket_afinet);
+ATF_TC_BODY(socket_afinet, tc)
+{
+ int sd;
+
+ sd = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_CHECK(sd >= 0);
+
+ close(sd);
+}
+
+ATF_TC_WITHOUT_HEAD(socket_afinet_bind_zero);
+ATF_TC_BODY(socket_afinet_bind_zero, tc)
+{
+ int sd, rc;
+ struct sockaddr_in sin;
+
+ if (atf_tc_get_config_var_as_bool_wd(tc, "ci", false))
+ atf_tc_skip("doesn't work when mac_portacl(4) loaded (https://bugs.freebsd.org/238781)");
+
+ sd = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_CHECK(sd >= 0);
+
+ bzero(&sin, sizeof(sin));
+ /*
+ * For AF_INET we do not check the family in in_pcbbind_setup(9),
+ * sa_len gets set from the syscall argument in getsockaddr(9),
+ * so we bind to 0:0.
+ */
+ rc = bind(sd, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+
+ close(sd);
+}
+
+ATF_TC_WITHOUT_HEAD(socket_afinet_bind_ok);
+ATF_TC_BODY(socket_afinet_bind_ok, tc)
+{
+ int sd, rc;
+ struct sockaddr_in sin;
+
+ sd = socket(PF_INET, SOCK_DGRAM, 0);
+ ATF_CHECK(sd >= 0);
+
+ bzero(&sin, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ rc = bind(sd, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+
+ close(sd);
+}
+
+ATF_TC_WITHOUT_HEAD(socket_afinet_poll_no_rdhup);
+ATF_TC_BODY(socket_afinet_poll_no_rdhup, tc)
+{
+ int ss, ss2, cs, rc;
+ struct sockaddr_in sin;
+ socklen_t slen;
+ struct pollfd pfd;
+ int one = 1;
+
+ /* Verify that we don't expose POLLRDHUP if not requested. */
+
+ /* Server setup. */
+ ss = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(ss >= 0);
+ rc = setsockopt(ss, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
+ ATF_CHECK_EQ(0, rc);
+ bzero(&sin, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ rc = bind(ss, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ rc = listen(ss, 1);
+ ATF_CHECK_EQ(0, rc);
+ slen = sizeof(sin);
+ rc = getsockname(ss, (struct sockaddr *)&sin, &slen);
+ ATF_CHECK_EQ(0, rc);
+
+ /* Client connects, server accepts. */
+ cs = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(cs >= 0);
+ rc = connect(cs, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ ss2 = accept(ss, NULL, NULL);
+ ATF_CHECK(ss2 >= 0);
+
+ /* Server can write, sees only POLLOUT. */
+ pfd.fd = ss2;
+ pfd.events = POLLIN | POLLOUT;
+ rc = poll(&pfd, 1, 0);
+ ATF_CHECK_EQ(1, rc);
+ ATF_CHECK_EQ(POLLOUT, pfd.revents);
+
+ /* Client closes socket! */
+ rc = close(cs);
+ ATF_CHECK_EQ(0, rc);
+
+ /*
+ * Server now sees POLLIN, but not POLLRDHUP because we didn't ask.
+ * Need non-zero timeout to wait for the FIN to arrive and trigger the
+ * socket to become readable.
+ */
+ pfd.fd = ss2;
+ pfd.events = POLLIN;
+ rc = poll(&pfd, 1, 60000);
+ ATF_CHECK_EQ(1, rc);
+ ATF_CHECK_EQ(POLLIN, pfd.revents);
+
+ close(ss2);
+ close(ss);
+}
+
+ATF_TC_WITHOUT_HEAD(socket_afinet_poll_rdhup);
+ATF_TC_BODY(socket_afinet_poll_rdhup, tc)
+{
+ int ss, ss2, cs, rc;
+ struct sockaddr_in sin;
+ socklen_t slen;
+ struct pollfd pfd;
+ char buffer;
+ int one = 1;
+
+ /* Verify that server sees POLLRDHUP if it asks for it. */
+
+ /* Server setup. */
+ ss = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(ss >= 0);
+ rc = setsockopt(ss, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
+ ATF_CHECK_EQ(0, rc);
+ bzero(&sin, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ rc = bind(ss, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ rc = listen(ss, 1);
+ ATF_CHECK_EQ(0, rc);
+ slen = sizeof(sin);
+ rc = getsockname(ss, (struct sockaddr *)&sin, &slen);
+ ATF_CHECK_EQ(0, rc);
+
+ /* Client connects, server accepts. */
+ cs = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(cs >= 0);
+ rc = connect(cs, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ ss2 = accept(ss, NULL, NULL);
+ ATF_CHECK(ss2 >= 0);
+
+ /* Server can write, so sees POLLOUT. */
+ pfd.fd = ss2;
+ pfd.events = POLLIN | POLLOUT | POLLRDHUP;
+ rc = poll(&pfd, 1, 0);
+ ATF_CHECK_EQ(1, rc);
+ ATF_CHECK_EQ(POLLOUT, pfd.revents);
+
+ /* Client writes two bytes, server reads only one of them. */
+ rc = write(cs, "xx", 2);
+ ATF_CHECK_EQ(2, rc);
+ rc = read(ss2, &buffer, 1);
+ ATF_CHECK_EQ(1, rc);
+
+ /* Server can read, so sees POLLIN. */
+ pfd.fd = ss2;
+ pfd.events = POLLIN | POLLOUT | POLLRDHUP;
+ rc = poll(&pfd, 1, 0);
+ ATF_CHECK_EQ(1, rc);
+ ATF_CHECK_EQ(POLLIN | POLLOUT, pfd.revents);
+
+ /* Client closes socket! */
+ rc = close(cs);
+ ATF_CHECK_EQ(0, rc);
+
+ /*
+ * Server sees Linux-style POLLRDHUP. Note that this is the case even
+ * though one byte of data remains unread.
+ *
+ * This races against the delivery of FIN caused by the close() above.
+ * Sometimes (more likely when run under truss or if another system
+ * call is added in between) it hits the path where sopoll_generic()
+ * immediately sees SBS_CANTRCVMORE, and sometimes it sleeps with flag
+ * SB_SEL so that it's woken up almost immediately and runs again,
+ * which is why we need a non-zero timeout here.
+ */
+ pfd.fd = ss2;
+ pfd.events = POLLRDHUP;
+ rc = poll(&pfd, 1, 60000);
+ ATF_CHECK_EQ(1, rc);
+ ATF_CHECK_EQ(POLLRDHUP, pfd.revents);
+
+ close(ss2);
+ close(ss);
+}
+
+ATF_TC_WITHOUT_HEAD(socket_afinet_stream_reconnect);
+ATF_TC_BODY(socket_afinet_stream_reconnect, tc)
+{
+ struct sockaddr_in sin;
+ socklen_t slen;
+ int ss, cs, rc;
+
+ /*
+ * Make sure that an attempt to connect(2) a connected or disconnected
+ * stream socket fails with EISCONN.
+ */
+
+ /* Server setup. */
+ ss = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(ss >= 0);
+ bzero(&sin, sizeof(sin));
+ sin.sin_family = AF_INET;
+ sin.sin_len = sizeof(sin);
+ sin.sin_port = htons(0);
+ sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ rc = bind(ss, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ rc = listen(ss, 1);
+ ATF_CHECK_EQ(0, rc);
+ slen = sizeof(sin);
+ rc = getsockname(ss, (struct sockaddr *)&sin, &slen);
+ ATF_CHECK_EQ(0, rc);
+
+ /* Client connects, shuts down. */
+ cs = socket(PF_INET, SOCK_STREAM, 0);
+ ATF_CHECK(cs >= 0);
+ rc = connect(cs, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(0, rc);
+ rc = shutdown(cs, SHUT_RDWR);
+ ATF_CHECK_EQ(0, rc);
+
+ /* A subsequent connect(2) fails with EISCONN. */
+ rc = connect(cs, (struct sockaddr *)&sin, sizeof(sin));
+ ATF_CHECK_EQ(-1, rc);
+ ATF_CHECK_EQ(errno, EISCONN);
+
+ rc = close(cs);
+ ATF_CHECK_EQ(0, rc);
+ rc = close(ss);
+ 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);
+ ATF_TP_ADD_TC(tp, socket_afinet_bind_zero);
+ ATF_TP_ADD_TC(tp, socket_afinet_bind_ok);
+ 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_connect_port_test.c b/tests/sys/netinet/tcp_connect_port_test.c
new file mode 100644
index 000000000000..ad087afb162d
--- /dev/null
+++ b/tests/sys/netinet/tcp_connect_port_test.c
@@ -0,0 +1,331 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2020 Netflix, Inc.
+ *
+ * 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 <sys/stat.h>
+#include <sys/sysctl.h>
+
+#include <netinet/in.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define SYSCTLBAKFILE "tmp.net.inet.ip.portrange.randomized"
+
+/*
+ * Check if port allocation is randomized. If so, update it. Save the old
+ * value of the sysctl so it can be updated later.
+ */
+static void
+disable_random_ports(void)
+{
+ int error, fd, random_new, random_save;
+ size_t sysctlsz;
+
+ /*
+ * Pre-emptively unlink our restoration file, so we will do no
+ * restoration on error.
+ */
+ unlink(SYSCTLBAKFILE);
+
+ /*
+ * Disable the net.inet.ip.portrange.randomized sysctl. Save the
+ * old value so we can restore it, if necessary.
+ */
+ random_new = 0;
+ sysctlsz = sizeof(random_save);
+ error = sysctlbyname("net.inet.ip.portrange.randomized", &random_save,
+ &sysctlsz, &random_new, sizeof(random_new));
+ if (error) {
+ warn("sysctlbyname(\"net.inet.ip.portrange.randomized\") "
+ "failed");
+ atf_tc_skip("Unable to set sysctl");
+ }
+ if (sysctlsz != sizeof(random_save)) {
+ fprintf(stderr, "Error: unexpected sysctl value size "
+ "(expected %zu, actual %zu)\n", sizeof(random_save),
+ sysctlsz);
+ goto restore_sysctl;
+ }
+
+ /* Open the backup file, write the contents, and close it. */
+ fd = open(SYSCTLBAKFILE, O_WRONLY|O_CREAT|O_TRUNC|O_EXCL,
+ S_IRUSR|S_IWUSR);
+ if (fd < 0) {
+ warn("error opening sysctl backup file");
+ goto restore_sysctl;
+ }
+ error = write(fd, &random_save, sizeof(random_save));
+ if (error < 0) {
+ warn("error writing saved value to sysctl backup file");
+ goto cleanup_and_restore;
+ }
+ if (error != (int)sizeof(random_save)) {
+ fprintf(stderr,
+ "Error writing saved value to sysctl backup file: "
+ "(expected %zu, actual %d)\n", sizeof(random_save), error);
+ goto cleanup_and_restore;
+ }
+ error = close(fd);
+ if (error) {
+ warn("error closing sysctl backup file");
+cleanup_and_restore:
+ (void)close(fd);
+ (void)unlink(SYSCTLBAKFILE);
+restore_sysctl:
+ (void)sysctlbyname("net.inet.ip.portrange.randomized", NULL,
+ NULL, &random_save, sysctlsz);
+ atf_tc_skip("Error setting sysctl");
+ }
+}
+
+/*
+ * Restore the sysctl value from the backup file and delete the backup file.
+ */
+static void
+restore_random_ports(void)
+{
+ int error, fd, random_save;
+
+ /* Open the backup file, read the contents, close it, and delete it. */
+ fd = open(SYSCTLBAKFILE, O_RDONLY);
+ if (fd < 0) {
+ warn("error opening sysctl backup file");
+ return;
+ }
+ error = read(fd, &random_save, sizeof(random_save));
+ if (error < 0) {
+ warn("error reading saved value from sysctl backup file");
+ return;
+ }
+ if (error != (int)sizeof(random_save)) {
+ fprintf(stderr,
+ "Error reading saved value from sysctl backup file: "
+ "(expected %zu, actual %d)\n", sizeof(random_save), error);
+ return;
+ }
+ error = close(fd);
+ if (error)
+ warn("error closing sysctl backup file");
+ error = unlink(SYSCTLBAKFILE);
+ if (error)
+ warn("error removing sysctl backup file");
+
+ /* Restore the saved sysctl value. */
+ error = sysctlbyname("net.inet.ip.portrange.randomized", NULL, NULL,
+ &random_save, sizeof(random_save));
+ if (error)
+ warn("sysctlbyname(\"net.inet.ip.portrange.randomized\") "
+ "failed while restoring value");
+}
+
+/*
+ * Given a domain and sockaddr, open a listening socket with automatic port
+ * selection. Then, try to connect 64K times. Ensure the connected socket never
+ * uses an overlapping port.
+ */
+static void
+connect_loop(int domain, const struct sockaddr *addr)
+{
+ union {
+ struct sockaddr saddr;
+ struct sockaddr_in saddr4;
+ struct sockaddr_in6 saddr6;
+ } su_clnt, su_srvr;
+ socklen_t salen;
+ int asock, csock, error, i, lsock;
+ const struct linger lopt = { 1, 0 };
+
+ /*
+ * Disable the net.inet.ip.portrange.randomized sysctl. Assuming an
+ * otherwise idle system, this makes the kernel try all possible
+ * ports sequentially and makes it more likely it will try the
+ * port on which we have a listening socket.
+ */
+ disable_random_ports();
+
+ /* Setup the listen socket. */
+ lsock = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(lsock >= 0, "socket() for listen socket failed: %s",
+ strerror(errno));
+ error = bind(lsock, addr, addr->sa_len);
+ ATF_REQUIRE_MSG(error == 0, "bind() failed: %s", strerror(errno));
+ error = listen(lsock, 1);
+ ATF_REQUIRE_MSG(error == 0, "listen() failed: %s", strerror(errno));
+
+ /*
+ * Get the address of the listen socket, which will be the destination
+ * address for our connection attempts.
+ */
+ salen = sizeof(su_srvr);
+ error = getsockname(lsock, &su_srvr.saddr, &salen);
+ ATF_REQUIRE_MSG(error == 0,
+ "getsockname() for listen socket failed: %s",
+ strerror(errno));
+ ATF_REQUIRE_MSG(salen == (domain == PF_INET ?
+ sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)),
+ "unexpected sockaddr size");
+ ATF_REQUIRE_MSG(su_srvr.saddr.sa_len == (domain == PF_INET ?
+ sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)),
+ "unexpected sa_len size");
+
+ /* Open 64K connections in a loop. */
+ for (i = 0; i < 65536; i++) {
+ csock = socket(domain, SOCK_STREAM, 0);
+ ATF_REQUIRE_MSG(csock >= 0,
+ "socket() for client socket %d failed: %s",
+ i, strerror(errno));
+
+ error = connect(csock, &su_srvr.saddr, su_srvr.saddr.sa_len);
+ ATF_REQUIRE_MSG(error == 0,
+ "connect() for client socket %d failed: %s",
+ i, strerror(errno));
+
+ error = setsockopt(csock, SOL_SOCKET, SO_LINGER, &lopt,
+ sizeof(lopt));
+ ATF_REQUIRE_MSG(error == 0,
+ "Setting linger for client socket %d failed: %s",
+ i, strerror(errno));
+
+ /* Ascertain the client socket address. */
+ salen = sizeof(su_clnt);
+ error = getsockname(csock, &su_clnt.saddr, &salen);
+ ATF_REQUIRE_MSG(error == 0,
+ "getsockname() for client socket %d failed: %s",
+ i, strerror(errno));
+ ATF_REQUIRE_MSG(salen == (domain == PF_INET ?
+ sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)),
+ "unexpected sockaddr size for client socket %d", i);
+
+ /* Ensure the ports do not match. */
+ switch (domain) {
+ case PF_INET:
+ ATF_REQUIRE_MSG(su_clnt.saddr4.sin_port !=
+ su_srvr.saddr4.sin_port,
+ "client socket %d using the same port as server",
+ i);
+ break;
+ case PF_INET6:
+ ATF_REQUIRE_MSG(su_clnt.saddr6.sin6_port !=
+ su_srvr.saddr6.sin6_port,
+ "client socket %d using the same port as server",
+ i);
+ break;
+ }
+
+ /* Accept the socket and close both ends. */
+ asock = accept(lsock, NULL, NULL);
+ ATF_REQUIRE_MSG(asock >= 0,
+ "accept() failed for client socket %d: %s",
+ i, strerror(errno));
+
+ error = close(asock);
+ ATF_REQUIRE_MSG(error == 0,
+ "close() failed for accepted socket %d: %s",
+ i, strerror(errno));
+
+ error = close(csock);
+ ATF_REQUIRE_MSG(error == 0,
+ "close() failed for client socket %d: %s",
+ i, strerror(errno));
+ }
+}
+
+ATF_TC_WITH_CLEANUP(basic_ipv4);
+ATF_TC_HEAD(basic_ipv4, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "require.config", "allow_sysctl_side_effects");
+ atf_tc_set_md_var(tc, "descr",
+ "Check automatic local port assignment during TCP connect calls");
+}
+
+ATF_TC_BODY(basic_ipv4, tc)
+{
+ struct sockaddr_in saddr4;
+
+ memset(&saddr4, 0, sizeof(saddr4));
+ saddr4.sin_len = sizeof(saddr4);
+ saddr4.sin_family = AF_INET;
+ saddr4.sin_port = htons(0);
+ saddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+
+ connect_loop(PF_INET, (const struct sockaddr *)&saddr4);
+}
+
+ATF_TC_CLEANUP(basic_ipv4, tc)
+{
+
+ restore_random_ports();
+}
+
+ATF_TC_WITH_CLEANUP(basic_ipv6);
+ATF_TC_HEAD(basic_ipv6, tc)
+{
+
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "require.config", "allow_sysctl_side_effects");
+ atf_tc_set_md_var(tc, "descr",
+ "Check automatic local port assignment during TCP connect calls");
+}
+
+ATF_TC_BODY(basic_ipv6, tc)
+{
+ struct sockaddr_in6 saddr6;
+
+ memset(&saddr6, 0, sizeof(saddr6));
+ saddr6.sin6_len = sizeof(saddr6);
+ saddr6.sin6_family = AF_INET6;
+ saddr6.sin6_port = htons(0);
+ saddr6.sin6_addr = in6addr_loopback;
+
+ connect_loop(PF_INET6, (const struct sockaddr *)&saddr6);
+}
+
+ATF_TC_CLEANUP(basic_ipv6, tc)
+{
+
+ restore_random_ports();
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, basic_ipv4);
+ ATF_TP_ADD_TC(tp, basic_ipv6);
+
+ return (atf_no_error());
+}
+
diff --git a/tests/sys/netinet/tcp_hpts_test.py b/tests/sys/netinet/tcp_hpts_test.py
new file mode 100644
index 000000000000..c56383fb310f
--- /dev/null
+++ b/tests/sys/netinet/tcp_hpts_test.py
@@ -0,0 +1,4 @@
+from atf_python.ktest import BaseKernelTest
+
+class TestTcpHpts(BaseKernelTest):
+ KTEST_MODULE_NAME = "ktest_tcphpts"
diff --git a/tests/sys/netinet/tcp_implied_connect.c b/tests/sys/netinet/tcp_implied_connect.c
new file mode 100644
index 000000000000..d03d6be4fb92
--- /dev/null
+++ b/tests/sys/netinet/tcp_implied_connect.c
@@ -0,0 +1,80 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 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 <stdio.h>
+#include <unistd.h>
+#include <string.h>
+
+#include <atf-c.h>
+
+ATF_TC_WITHOUT_HEAD(tcp_implied_connect);
+ATF_TC_BODY(tcp_implied_connect, tc)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(sin),
+ };
+ const char buf[] = "hello";
+ char repl[sizeof(buf)];
+ socklen_t len;
+ int s, c, a;
+
+ ATF_REQUIRE(s = socket(PF_INET, SOCK_STREAM, 0));
+ ATF_REQUIRE(c = socket(PF_INET, SOCK_STREAM, 0));
+
+ 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
+ /*
+ * The disabled code is that you would normally do.
+ */
+ ATF_REQUIRE(connect(c, (struct sockaddr *)&sin, sizeof(sin)) == 0);
+ ATF_REQUIRE(send(c, &buf, sizeof(buf), 0) == sizeof(buf));
+#else
+ /*
+ * And this is implied connect.
+ */
+ ATF_REQUIRE(sendto(c, &buf, sizeof(buf), 0, (struct sockaddr *)&sin,
+ sizeof(sin)) == sizeof(buf));
+#endif
+
+ ATF_REQUIRE((a = accept(s, NULL, NULL)) != 1);
+ ATF_REQUIRE(recv(a, &repl, sizeof(repl), 0) == sizeof(buf));
+ ATF_REQUIRE(strcmp(buf, repl) == 0);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, tcp_implied_connect);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/netinet/tcp_md5_getsockopt.c b/tests/sys/netinet/tcp_md5_getsockopt.c
new file mode 100644
index 000000000000..e23cfa67185a
--- /dev/null
+++ b/tests/sys/netinet/tcp_md5_getsockopt.c
@@ -0,0 +1,134 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2022, Klara Inc.
+ * Copyright (c) 2022, Claudio Jeker <claudio@openbsd.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/linker.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <err.h>
+
+#include <atf-c.h>
+
+void test_tcp_md5_getsockopt(int);
+
+void
+test_tcp_md5_getsockopt(int v6)
+{
+ struct sockaddr_in *s;
+ struct sockaddr_in6 s6 = { 0 };
+ struct sockaddr_in s4 = { 0 };
+ socklen_t len;
+ int csock, ssock, opt;
+ int pf;
+
+ if (v6) {
+ pf = PF_INET6;
+ len = sizeof(s6);
+
+ s6.sin6_family = pf;
+ s6.sin6_len = sizeof(s6);
+ s6.sin6_addr = in6addr_loopback;
+ s6.sin6_port = 0;
+
+ s = (struct sockaddr_in *)&s6;
+ } else {
+ pf = PF_INET;
+ len = sizeof(s4);
+
+ s4.sin_family = pf;
+ s4.sin_len = sizeof(s4);
+ s4.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ s4.sin_port = 0;
+
+ s = &s4;
+ }
+
+ if ((ssock = socket(pf, SOCK_STREAM, 0)) == -1)
+ atf_tc_fail("creating server socket");
+
+ fcntl(ssock, F_SETFL, O_NONBLOCK);
+
+ if ((bind(ssock, (struct sockaddr *)s, len) == -1))
+ atf_tc_fail("binding to localhost");
+
+ getsockname(ssock, (struct sockaddr *)s, &len);
+
+ listen(ssock, 1);
+
+ if ((csock = socket(pf, SOCK_STREAM, 0)) == -1)
+ atf_tc_fail("creating client socket");
+
+ if (connect(csock, (struct sockaddr *)s, len) == -1)
+ atf_tc_fail("connecting to server instance");
+
+ if (getsockopt(csock, IPPROTO_TCP, TCP_MD5SIG, &opt, &len) == -1)
+ atf_tc_fail("getsockopt");
+
+ close(csock);
+ close(ssock);
+
+ atf_tc_pass();
+}
+
+ATF_TC(tcp_md5_getsockopt_v4);
+ATF_TC_HEAD(tcp_md5_getsockopt_v4, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Test getsockopt for TCP MD5 SIG (IPv4)");
+ atf_tc_set_md_var(tc, "require.kmods", "tcpmd5");
+}
+
+ATF_TC_BODY(tcp_md5_getsockopt_v4, tc)
+{
+ test_tcp_md5_getsockopt(0);
+}
+
+ATF_TC(tcp_md5_getsockopt_v6);
+ATF_TC_HEAD(tcp_md5_getsockopt_v6, tc)
+{
+ atf_tc_set_md_var(tc, "descr", "Test getsockopt for TCP MD5 SIG (IPv6)");
+ atf_tc_set_md_var(tc, "require.kmods", "tcpmd5");
+}
+
+ATF_TC_BODY(tcp_md5_getsockopt_v6, tc)
+{
+ test_tcp_md5_getsockopt(1);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, tcp_md5_getsockopt_v4);
+ ATF_TP_ADD_TC(tp, tcp_md5_getsockopt_v6);
+
+ return atf_no_error();
+}
diff --git a/tests/sys/netinet/tcp_user_cookie.c b/tests/sys/netinet/tcp_user_cookie.c
new file mode 100644
index 000000000000..51df4860f6bd
--- /dev/null
+++ b/tests/sys/netinet/tcp_user_cookie.c
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2016 Limelight Networks
+ * 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,
+ * without modification.
+ * 2. Redistributions in binary form must reproduce at minimum a disclaimer
+ * substantially similar to the "NO WARRANTY" disclaimer below
+ * ("Disclaimer") and any redistribution must be conditioned upon
+ * including a substantially similar Disclaimer requirement for further
+ * binary redistribution.
+ *
+ * NO WARRANTY
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES.
+ *
+ * Authors: George Neville-Neil
+ */
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <err.h>
+#include <sysexits.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define buflen 80
+
+/*
+ * Setup a TCP server listening on a port for connections, all of
+ * which subseuqently have their user cookie set.
+ */
+int
+main(int argc, char **argv)
+{
+ struct sockaddr_in srv;
+ int sock, accepted, port, cookie;
+ int ret;
+ char recvbuf[buflen];
+
+ if (argc != 3) {
+ fprintf(stderr, "Usage: %s port cookie\n", argv[0]);
+ exit(2);
+ }
+
+ port = atoi(argv[1]);
+ cookie = atoi(argv[2]);
+
+ sock = socket(PF_INET, SOCK_STREAM, 0);
+ if (sock < 0)
+ err(EXIT_FAILURE, "socket");
+
+ memset(&srv, 0, sizeof(srv));
+ srv.sin_len = sizeof(srv);
+ srv.sin_family = AF_INET;
+ srv.sin_port = htons(port);
+ srv.sin_addr.s_addr = INADDR_ANY;
+
+ if (bind(sock, (struct sockaddr *)&srv, srv.sin_len) < 0)
+ err(EX_OSERR, "failed to bind to port %d", port);
+
+ if (listen(sock, 5) < 0)
+ err(EX_OSERR, "failed to listen on socket");
+
+ ret = setsockopt(sock, SOL_SOCKET, SO_USER_COOKIE, &cookie, sizeof(cookie));
+ if (ret < 0)
+ err(EX_OSERR, "setsockopt(SO_USER_COOKIE)");
+
+ while (1) {
+
+ accepted = accept(sock, NULL, 0);
+
+ if (accepted < 0)
+ err(EX_OSERR, "accept failed");
+
+ ret = setsockopt(accepted, SOL_SOCKET, SO_USER_COOKIE,
+ &cookie, sizeof(cookie));
+ if (ret < 0)
+ err(EX_OSERR, "setsockopt(SO_USER_COOKIE)");
+
+ ret = read(accepted, &recvbuf, buflen);
+
+ if (ret < 0)
+ warn("failed read");
+
+ close(accepted);
+ }
+
+ return (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_dontroute.c b/tests/sys/netinet/udp_dontroute.c
new file mode 100644
index 000000000000..ba6d4ec6236c
--- /dev/null
+++ b/tests/sys/netinet/udp_dontroute.c
@@ -0,0 +1,136 @@
+/*
+ * Copyright (c) 2014 Spectra Logic Corporation
+ * 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,
+ * without modification.
+ * 2. Redistributions in binary form must reproduce at minimum a disclaimer
+ * substantially similar to the "NO WARRANTY" disclaimer below
+ * ("Disclaimer") and any redistribution must be conditioned upon
+ * including a substantially similar Disclaimer requirement for further
+ * binary redistribution.
+ *
+ * NO WARRANTY
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDERS OR CONTRIBUTORS BE LIABLE FOR 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 DAMAGES.
+ *
+ * Authors: Alan Somers (Spectra Logic Corporation)
+ */
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/*
+ * Sends a single UDP packet to the provided address, with SO_DONTROUTE set
+ * I couldn't find a way to do this with builtin utilities like nc(1)
+ */
+int
+main(int argc, char **argv)
+{
+ struct sockaddr_storage dst;
+ int s, t;
+ int opt;
+ int ret;
+ ssize_t len;
+ const char* sendbuf = "Hello, World!";
+ const size_t buflen = 80;
+ char recvbuf[buflen];
+ bool v6 = false;
+ const char *addr, *tapdev;
+ const uint16_t port = 46120;
+
+ bzero(&dst, sizeof(dst));
+ if (argc < 3 || argc > 4) {
+ fprintf(stderr, "Usage: %s [-6] ip_address tapdev\n", argv[0]);
+ exit(2);
+ }
+
+ if (strcmp("-6", argv[1]) == 0) {
+ v6 = true;
+ addr = argv[2];
+ tapdev = argv[3];
+ } else {
+ addr = argv[1];
+ tapdev = argv[2];
+ }
+
+ t = open(tapdev, O_RDWR | O_NONBLOCK);
+ if (t < 0)
+ err(EXIT_FAILURE, "open");
+
+ if (v6)
+ s = socket(PF_INET6, SOCK_DGRAM, 0);
+ else
+ s = socket(PF_INET, SOCK_DGRAM, 0);
+ if (s < 0)
+ err(EXIT_FAILURE, "socket");
+ opt = 1;
+
+ ret = setsockopt(s, SOL_SOCKET, SO_DONTROUTE, &opt, sizeof(opt));
+ if (ret == -1)
+ err(EXIT_FAILURE, "setsockopt(SO_DONTROUTE)");
+
+ if (v6) {
+ struct sockaddr_in6 *dst6 = ((struct sockaddr_in6*)&dst);
+
+ dst.ss_len = sizeof(struct sockaddr_in6);
+ dst.ss_family = AF_INET6;
+ dst6->sin6_port = htons(port);
+ ret = inet_pton(AF_INET6, addr, &dst6->sin6_addr);
+ } else {
+ struct sockaddr_in *dst4 = ((struct sockaddr_in*)&dst);
+
+ dst.ss_len = sizeof(struct sockaddr_in);
+ dst.ss_family = AF_INET;
+ dst4->sin_port = htons(port);
+ ret = inet_pton(AF_INET, addr, &dst4->sin_addr);
+ }
+ if (ret != 1)
+ err(EXIT_FAILURE, "inet_pton returned %d", ret);
+
+ ret = sendto(s, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&dst,
+ dst.ss_len);
+ if (ret == -1)
+ err(EXIT_FAILURE, "sendto");
+
+ /* Verify that the packet went to the desired tap device */
+
+ len = read(t, recvbuf, buflen);
+ if (len == 0)
+ errx(EXIT_FAILURE, "read returned EOF");
+ else if (len < 0 && errno == EAGAIN)
+ errx(EXIT_FAILURE, "Did not receive any packets");
+ else if (len < 0)
+ err(EXIT_FAILURE, "read");
+
+ /*
+ * If read returned anything at all, consider it a success. The packet
+ * should be an Ethernet frame containing an ARP request for
+ * ip_address. We won't bother to decode it
+ */
+ return (0);
+}
diff --git a/tests/sys/netinet/udp_io.c b/tests/sys/netinet/udp_io.c
new file mode 100644
index 000000000000..04f9bf56ed02
--- /dev/null
+++ b/tests/sys/netinet/udp_io.c
@@ -0,0 +1,140 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2024 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 <errno.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+/*
+ * Create a pair of UDP sockets. The first one is bound to a local
+ * address and the second one is connected to it.
+ */
+static void
+udp_socketpair(int *s)
+{
+ struct sockaddr_in sin = {
+ .sin_family = AF_INET,
+ .sin_len = sizeof(sin),
+ };
+ socklen_t slen = sizeof(sin);
+ int b, c;
+
+ ATF_REQUIRE((b = socket(PF_INET, SOCK_DGRAM, 0)) > 0);
+ 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;
+ s[1] = c;
+}
+
+/*
+ * Check MSG_TRUNC.
+ */
+ATF_TC_WITHOUT_HEAD(trunc);
+ATF_TC_BODY(trunc, tc)
+{
+ char sbuf[] = "Hello, peer!", rbuf[sizeof(sbuf)];
+ struct iovec iov = {
+ .iov_base = sbuf,
+ .iov_len = sizeof(sbuf),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+ int s[2];
+ u_int n;
+
+ udp_socketpair(s);
+
+ ATF_REQUIRE(sendmsg(s[1], &msg, 0) == sizeof(sbuf));
+ n = (arc4random() % (sizeof(sbuf) - 1)) + 1;
+ iov.iov_base = rbuf;
+ iov.iov_len = n;
+ ATF_REQUIRE(recvmsg(s[0], &msg, 0) == (ssize_t)n);
+ ATF_REQUIRE(msg.msg_flags == MSG_TRUNC);
+ ATF_REQUIRE(strncmp(sbuf, rbuf, n) == 0);
+ iov.iov_len = sizeof(rbuf);
+ ATF_REQUIRE(recvmsg(s[0], &msg, MSG_DONTWAIT) == -1);
+ ATF_REQUIRE(errno == EAGAIN);
+
+ close(s[0]);
+ close(s[1]);
+}
+
+/*
+ * Check MSG_PEEK.
+ */
+ATF_TC_WITHOUT_HEAD(peek);
+ATF_TC_BODY(peek, tc)
+{
+ char sbuf[] = "Hello, peer!", rbuf[sizeof(sbuf)];
+ struct iovec iov = {
+ .iov_base = sbuf,
+ .iov_len = sizeof(sbuf),
+ };
+ struct msghdr msg = {
+ .msg_iov = &iov,
+ .msg_iovlen = 1,
+ };
+ int s[2];
+ u_int n;
+
+ udp_socketpair(s);
+
+ ATF_REQUIRE(sendmsg(s[1], &msg, 0) == sizeof(sbuf));
+ iov.iov_base = rbuf;
+ for (int i = 0; i < 10; i++) {
+ n = (arc4random() % sizeof(sbuf)) + 1;
+ iov.iov_len = n;
+ ATF_REQUIRE(recvmsg(s[0], &msg, MSG_PEEK) == (ssize_t)n);
+ if (n < sizeof(sbuf))
+ ATF_REQUIRE(msg.msg_flags == (MSG_PEEK | MSG_TRUNC));
+ else
+ ATF_REQUIRE(msg.msg_flags == MSG_PEEK);
+ ATF_REQUIRE(strncmp(sbuf, rbuf, n) == 0);
+ }
+
+ close(s[0]);
+ close(s[1]);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, trunc);
+ ATF_TP_ADD_TC(tp, peek);
+
+ return (atf_no_error());
+}