diff options
Diffstat (limited to 'tests/sys/netinet')
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()); +} |
