git: ec00e95190ad - main - netinet tests: Add test for IPv6 mapped-v4 bind problem

From: Mike Karels <karels_at_FreeBSD.org>
Date: Tue, 09 Aug 2022 12:09:40 UTC
The branch main has been updated by karels:

URL: https://cgit.FreeBSD.org/src/commit/?id=ec00e95190ad9c54222d5c30e6156e375769478e

commit ec00e95190ad9c54222d5c30e6156e375769478e
Author:     Mike Karels <karels@FreeBSD.org>
AuthorDate: 2022-08-09 12:08:09 +0000
Commit:     Mike Karels <karels@FreeBSD.org>
CommitDate: 2022-08-09 12:08:09 +0000

    netinet tests: Add test for IPv6 mapped-v4 bind problem
    
    Test fix in 637f317c6d9c, verifying that when ports run out, we get
    an EADDRNOTAVAIL error from bind() rather than an EADDRINUSE error
    from connect().  Use small port range to exhaust ports and see which
    error happens.
    
    Reviewed by:    tuexen, glebius, melifaro
    Differential Revision:  https://reviews.freebsd.org/D36056
    MFC after:      3 days (with 637f317c6d9c)
---
 tests/sys/netinet/Makefile                  |   2 +
 tests/sys/netinet/tcp6_v4mapped_bind_test.c | 333 ++++++++++++++++++++++++++++
 2 files changed, 335 insertions(+)

diff --git a/tests/sys/netinet/Makefile b/tests/sys/netinet/Makefile
index f21e254cd30f..7bcbfc202700 100644
--- a/tests/sys/netinet/Makefile
+++ b/tests/sys/netinet/Makefile
@@ -10,11 +10,13 @@ TESTS_SUBDIRS+=	libalias
 ATF_TESTS_C=	ip_reass_test \
 		so_reuseport_lb_test \
 		socket_afinet \
+		tcp6_v4mapped_bind_test \
 		tcp_connect_port_test \
 		tcp_md5_getsockopt
 
 ATF_TESTS_SH=	carp fibs fibs_test redirect divert forward output lpm arp
 TEST_METADATA.output+=	required_programs="python"
+TEST_METADATA.tcp6_v4mapped_bind_test+=	is_exclusive="true"
 
 PROGS=	udp_dontroute tcp_user_cookie
 
diff --git a/tests/sys/netinet/tcp6_v4mapped_bind_test.c b/tests/sys/netinet/tcp6_v4mapped_bind_test.c
new file mode 100644
index 000000000000..0d768500a150
--- /dev/null
+++ b/tests/sys/netinet/tcp6_v4mapped_bind_test.c
@@ -0,0 +1,333 @@
+/*-
+ * 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/cdefs.h>
+__FBSDID("$FreeBSD$");
+
+#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(v4mapped);
+ATF_TC_HEAD(v4mapped, 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(v4mapped, 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_REQUIRE_MSG(error == 0,
+			    "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_TC_CLEANUP(v4mapped, tc)
+{
+
+	restore_portrange();
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, v4mapped);
+
+	return (atf_no_error());
+}
+