git: 2f3f9c9d54bb - stable/14 - pf: fix pf divert-to loop

From: Kristof Provost <kp_at_FreeBSD.org>
Date: Thu, 09 Nov 2023 14:39:20 UTC
The branch stable/14 has been updated by kp:

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

commit 2f3f9c9d54bb274dfb5de40f4ce7ca944d4e05a5
Author:     Igor Ostapenko <pm@igoro.pro>
AuthorDate: 2023-10-19 10:12:15 +0000
Commit:     Kristof Provost <kp@FreeBSD.org>
CommitDate: 2023-11-09 09:55:45 +0000

    pf: fix pf divert-to loop
    
    Resolved conflict between ipfw and pf if both are used and pf wants to
    do divert(4) by having separate mtags for pf and ipfw.
    
    Also fix the incorrect 'rulenum' check, which caused the reported loop.
    
    While here add a few test cases to ensure that divert-to works as
    expected, even if ipfw is loaded.
    
    divert(4)
    PR:             272770
    MFC after:      3 weeks
    Reviewed by:    kp
    Differential Revision:  https://reviews.freebsd.org/D42142
    
    (cherry picked from commit fabf705f4b5aff2fa2dc997c2d0afd62a6927e68)
---
 sys/netinet/ip_divert.c           |  31 ++-
 sys/netinet/ip_var.h              |  10 +
 sys/netpfil/pf/pf.c               |  32 ++-
 tests/sys/netpfil/pf/Makefile     |   4 +
 tests/sys/netpfil/pf/divapp.c     | 149 ++++++++++++++
 tests/sys/netpfil/pf/divert-to.sh | 413 ++++++++++++++++++++++++++++++++++++++
 6 files changed, 625 insertions(+), 14 deletions(-)

diff --git a/sys/netinet/ip_divert.c b/sys/netinet/ip_divert.c
index dd0a8b74c013..ad95a1ce0d76 100644
--- a/sys/netinet/ip_divert.c
+++ b/sys/netinet/ip_divert.c
@@ -171,11 +171,19 @@ divert_packet(struct mbuf *m, bool incoming)
 	u_int16_t nport;
 	struct sockaddr_in divsrc;
 	struct m_tag *mtag;
+	uint16_t cookie;
 
 	NET_EPOCH_ASSERT();
 
 	mtag = m_tag_locate(m, MTAG_IPFW_RULE, 0, NULL);
-	if (mtag == NULL) {
+	if (mtag != NULL) {
+		cookie = ((struct ipfw_rule_ref *)(mtag+1))->rulenum;
+		nport = htons((uint16_t)
+		    (((struct ipfw_rule_ref *)(mtag+1))->info));
+	} else if ((mtag = m_tag_locate(m, MTAG_PF_DIVERT, 0, NULL)) != NULL) {
+		cookie = ((struct pf_divert_mtag *)(mtag+1))->idir;
+		nport = htons(((struct pf_divert_mtag *)(mtag+1))->ndir);
+	} else {
 		m_freem(m);
 		return;
 	}
@@ -216,7 +224,7 @@ divert_packet(struct mbuf *m, bool incoming)
 	divsrc.sin_len = sizeof(divsrc);
 	divsrc.sin_family = AF_INET;
 	/* record matching rule, in host format */
-	divsrc.sin_port = ((struct ipfw_rule_ref *)(mtag+1))->rulenum;
+	divsrc.sin_port = cookie;
 	/*
 	 * Record receive interface address, if any.
 	 * But only for incoming packets.
@@ -265,7 +273,6 @@ divert_packet(struct mbuf *m, bool incoming)
 	}
 
 	/* Put packet on socket queue, if any */
-	nport = htons((uint16_t)(((struct ipfw_rule_ref *)(mtag+1))->info));
 	SLIST_FOREACH(dcb, &V_divhash[DIVHASH(nport)], dcb_next)
 		if (dcb->dcb_port == nport)
 			break;
@@ -304,6 +311,7 @@ div_send(struct socket *so, int flags, struct mbuf *m, struct sockaddr *nam,
 	const struct ip *ip;
 	struct m_tag *mtag;
 	struct ipfw_rule_ref *dt;
+	struct pf_divert_mtag *pfdt;
 	int error, family;
 
 	if (control)
@@ -390,13 +398,30 @@ div_send(struct socket *so, int flags, struct mbuf *m, struct sockaddr *nam,
 		return (EAFNOSUPPORT);
 	}
 
+	mtag = m_tag_locate(m, MTAG_PF_DIVERT, 0, NULL);
+	if (mtag == NULL) {
+		/* this should be normal */
+		mtag = m_tag_alloc(MTAG_PF_DIVERT, 0,
+		    sizeof(struct pf_divert_mtag), M_NOWAIT | M_ZERO);
+		if (mtag == NULL) {
+			m_freem(m);
+			return (ENOBUFS);
+		}
+		m_tag_prepend(m, mtag);
+	}
+	pfdt = (struct pf_divert_mtag *)(mtag+1);
+	if (sin)
+		pfdt->idir = sin->sin_port;
+
 	/* Reinject packet into the system as incoming or outgoing */
 	NET_EPOCH_ENTER(et);
 	if (!sin || sin->sin_addr.s_addr == 0) {
 		dt->info |= IPFW_IS_DIVERT | IPFW_INFO_OUT;
+		pfdt->ndir = PF_DIVERT_MTAG_DIR_OUT;
 		error = div_output_outbound(family, so, m);
 	} else {
 		dt->info |= IPFW_IS_DIVERT | IPFW_INFO_IN;
+		pfdt->ndir = PF_DIVERT_MTAG_DIR_IN;
 		error = div_output_inbound(family, so, m, sin);
 	}
 	NET_EPOCH_EXIT(et);
diff --git a/sys/netinet/ip_var.h b/sys/netinet/ip_var.h
index 06560fb52afe..a8c687682af9 100644
--- a/sys/netinet/ip_var.h
+++ b/sys/netinet/ip_var.h
@@ -326,6 +326,16 @@ extern void	(*ip_divert_ptr)(struct mbuf *m, bool incoming);
 extern int	(*ng_ipfw_input_p)(struct mbuf **, struct ip_fw_args *, bool);
 extern int	(*ip_dn_ctl_ptr)(struct sockopt *);
 extern int	(*ip_dn_io_ptr)(struct mbuf **, struct ip_fw_args *);
+
+/* pf specific mtag for divert(4) support */
+enum { PF_DIVERT_MTAG_DIR_IN=1, PF_DIVERT_MTAG_DIR_OUT=2 };
+struct pf_divert_mtag {
+	uint16_t idir;	// initial pkt direction
+	uint16_t ndir;	// a) divert(4) port upon initial diversion
+			// b) new direction upon pkt re-enter
+};
+#define MTAG_PF_DIVERT	1262273569
+
 #endif /* _KERNEL */
 
 #endif /* !_NETINET_IP_VAR_H_ */
diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index b80ec2bb303d..eb2e09b2e6f2 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -7855,7 +7855,7 @@ pf_test(int dir, int pflags, struct ifnet *ifp, struct mbuf **m0,
 	u_short			 action, reason = 0;
 	struct mbuf		*m = *m0;
 	struct ip		*h = NULL;
-	struct m_tag		*ipfwtag;
+	struct m_tag		*mtag;
 	struct pf_krule		*a = NULL, *r = &V_pf_default_rule, *tr, *nr;
 	struct pf_kstate	*s = NULL;
 	struct pf_kruleset	*ruleset = NULL;
@@ -7945,21 +7945,26 @@ pf_test(int dir, int pflags, struct ifnet *ifp, struct mbuf **m0,
 	off = h->ip_hl << 2;
 
 	if (__predict_false(ip_divert_ptr != NULL) &&
-	    ((ipfwtag = m_tag_locate(m, MTAG_IPFW_RULE, 0, NULL)) != NULL)) {
-		struct ipfw_rule_ref *rr = (struct ipfw_rule_ref *)(ipfwtag+1);
-		if (rr->info & IPFW_IS_DIVERT && rr->rulenum == 0) {
+	    ((mtag = m_tag_locate(m, MTAG_PF_DIVERT, 0, NULL)) != NULL)) {
+		struct pf_divert_mtag *dt = (struct pf_divert_mtag *)(mtag+1);
+		if ((dt->idir == PF_DIVERT_MTAG_DIR_IN && dir == PF_IN) ||
+		    (dt->idir == PF_DIVERT_MTAG_DIR_OUT && dir == PF_OUT)) {
 			if (pd.pf_mtag == NULL &&
 			    ((pd.pf_mtag = pf_get_mtag(m)) == NULL)) {
 				action = PF_DROP;
 				goto done;
 			}
 			pd.pf_mtag->flags |= PF_MTAG_FLAG_PACKET_LOOPED;
-			m_tag_delete(m, ipfwtag);
 		}
 		if (pd.pf_mtag && pd.pf_mtag->flags & PF_MTAG_FLAG_FASTFWD_OURS_PRESENT) {
 			m->m_flags |= M_FASTFWD_OURS;
 			pd.pf_mtag->flags &= ~PF_MTAG_FLAG_FASTFWD_OURS_PRESENT;
 		}
+		m_tag_delete(m, mtag);
+
+		mtag = m_tag_locate(m, MTAG_IPFW_RULE, 0, NULL);
+		if (mtag != NULL)
+			m_tag_delete(m, mtag);
 	} else if (pf_normalize_ip(m0, kif, &reason, &pd) != PF_PASS) {
 		/* We do IP header normalization and packet reassembly here */
 		action = PF_DROP;
@@ -8241,17 +8246,19 @@ done:
 
 	if (__predict_false(ip_divert_ptr != NULL) && action == PF_PASS &&
 	    r->divert.port && !PACKET_LOOPED(&pd)) {
-		ipfwtag = m_tag_alloc(MTAG_IPFW_RULE, 0,
-		    sizeof(struct ipfw_rule_ref), M_NOWAIT | M_ZERO);
-		if (ipfwtag != NULL) {
-			((struct ipfw_rule_ref *)(ipfwtag+1))->info =
+		mtag = m_tag_alloc(MTAG_PF_DIVERT, 0,
+		    sizeof(struct pf_divert_mtag), M_NOWAIT | M_ZERO);
+		if (mtag != NULL) {
+			((struct pf_divert_mtag *)(mtag+1))->ndir =
 			    ntohs(r->divert.port);
-			((struct ipfw_rule_ref *)(ipfwtag+1))->rulenum = dir;
+			((struct pf_divert_mtag *)(mtag+1))->idir =
+			    (dir == PF_IN) ? PF_DIVERT_MTAG_DIR_IN :
+			    PF_DIVERT_MTAG_DIR_OUT;
 
 			if (s)
 				PF_STATE_UNLOCK(s);
 
-			m_tag_prepend(m, ipfwtag);
+			m_tag_prepend(m, mtag);
 			if (m->m_flags & M_FASTFWD_OURS) {
 				if (pd.pf_mtag == NULL &&
 				    ((pd.pf_mtag = pf_get_mtag(m)) == NULL)) {
@@ -8279,6 +8286,9 @@ done:
 			    ("pf: failed to allocate divert tag\n"));
 		}
 	}
+	/* this flag will need revising if the pkt is forwarded */
+	if (pd.pf_mtag)
+		pd.pf_mtag->flags &= ~PF_MTAG_FLAG_PACKET_LOOPED;
 
 	if (pd.act.log) {
 		struct pf_krule		*lr;
diff --git a/tests/sys/netpfil/pf/Makefile b/tests/sys/netpfil/pf/Makefile
index 44fe95680dfb..c9adab5626d4 100644
--- a/tests/sys/netpfil/pf/Makefile
+++ b/tests/sys/netpfil/pf/Makefile
@@ -2,10 +2,12 @@
 PACKAGE=	tests
 
 TESTSDIR=       ${TESTSBASE}/sys/netpfil/pf
+BINDIR=		${TESTSDIR}
 TESTS_SUBDIRS+=	ioctl
 
 ATF_TESTS_SH+=	altq \
 		anchor \
+		divert-to \
 		dup \
 		ether \
 		forward \
@@ -45,6 +47,8 @@ ATF_TESTS_PYTEST+=	sctp.py
 # Tests reuse jail names and so cannot run in parallel.
 TEST_METADATA+=	is_exclusive=true
 
+PROGS=	divapp
+
 ${PACKAGE}FILES+=	CVE-2019-5597.py \
 			CVE-2019-5598.py \
 			daytime_inetd.conf \
diff --git a/tests/sys/netpfil/pf/divapp.c b/tests/sys/netpfil/pf/divapp.c
new file mode 100644
index 000000000000..908c41eaa67f
--- /dev/null
+++ b/tests/sys/netpfil/pf/divapp.c
@@ -0,0 +1,149 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE PROJECT 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 PROJECT 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.
+ */
+
+/* Used by tests like divert-to.sh */
+
+#include <errno.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <err.h>
+#include <sysexits.h>
+#include <string.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <netinet/ip.h>
+
+
+struct context {
+	unsigned short divert_port;
+	bool divert_back;
+
+	int fd;
+	struct sockaddr_in sin;
+	socklen_t sin_len;
+	char pkt[IP_MAXPACKET];
+	ssize_t pkt_n;
+};
+
+static void
+init(struct context *c)
+{
+	c->fd = socket(PF_DIVERT, SOCK_RAW, 0);
+	if (c->fd == -1)
+		errx(EX_OSERR, "init: Cannot create divert socket.");
+
+	memset(&c->sin, 0, sizeof(c->sin));
+	c->sin.sin_family = AF_INET;
+	c->sin.sin_port = htons(c->divert_port);
+	c->sin.sin_addr.s_addr = INADDR_ANY;
+	c->sin_len = sizeof(struct sockaddr_in);
+
+	if (bind(c->fd, (struct sockaddr *) &c->sin, c->sin_len) != 0)
+		errx(EX_OSERR, "init: Cannot bind divert socket.");
+}
+
+static ssize_t
+recv_pkt(struct context *c)
+{
+	fd_set readfds;
+	struct timeval timeout;
+	int s;
+
+	FD_ZERO(&readfds);
+	FD_SET(c->fd, &readfds);
+	timeout.tv_sec = 3;
+	timeout.tv_usec = 0;
+
+	s = select(c->fd + 1, &readfds, 0, 0, &timeout);
+	if (s == -1)
+		errx(EX_IOERR, "recv_pkt: select() errors.");
+	if (s != 1) // timeout
+		return -1;
+
+	c->pkt_n = recvfrom(c->fd, c->pkt, sizeof(c->pkt), 0,
+	    (struct sockaddr *) &c->sin, &c->sin_len);
+	if (c->pkt_n == -1)
+		errx(EX_IOERR, "recv_pkt: recvfrom() errors.");
+
+	return (c->pkt_n);
+}
+
+static void
+send_pkt(struct context *c)
+{
+	ssize_t n;
+	char errstr[32];
+
+	n = sendto(c->fd, c->pkt, c->pkt_n, 0,
+	    (struct sockaddr *) &c->sin, c->sin_len);
+	if (n == -1) {
+		strerror_r(errno, errstr, sizeof(errstr));
+		errx(EX_IOERR, "send_pkt: sendto() errors: %d %s.", errno, errstr);
+	}
+	if (n != c->pkt_n)
+		errx(EX_IOERR, "send_pkt: sendto() sent %zd of %zd bytes.",
+		    n, c->pkt_n);
+}
+
+int
+main(int argc, char *argv[])
+{
+	struct context c;
+	int npkt;
+
+	if (argc < 2)
+		errx(EX_USAGE,
+		    "Usage: %s <divert-port> [divert-back]", argv[0]);
+
+	memset(&c, 0, sizeof(struct context));
+
+	c.divert_port = (unsigned short) strtol(argv[1], NULL, 10);
+	if (c.divert_port == 0)
+		errx(EX_USAGE, "divert port is not defined.");
+
+	if (argc >= 3 && strcmp(argv[2], "divert-back") == 0)
+		c.divert_back = true;
+
+
+	init(&c);
+
+	npkt = 0;
+	while (recv_pkt(&c) > 0) {
+		if (c.divert_back)
+			send_pkt(&c);
+		npkt++;
+		if (npkt >= 10)
+			break;
+	}
+
+	if (npkt != 1)
+		errx(EXIT_FAILURE, "%d: npkt=%d.", c.divert_port, npkt);
+
+	return EXIT_SUCCESS;
+}
diff --git a/tests/sys/netpfil/pf/divert-to.sh b/tests/sys/netpfil/pf/divert-to.sh
new file mode 100644
index 000000000000..0a37cea78ad3
--- /dev/null
+++ b/tests/sys/netpfil/pf/divert-to.sh
@@ -0,0 +1,413 @@
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2023 Igor Ostapenko <pm@igoro.pro>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+#
+# pf divert-to action test cases
+#
+#  -----------|           |--     |----|     ----|            |-----------
+# ( ) inbound |pf_check_in|  ) -> |host| -> ( )  |pf_check_out| outbound  )
+#  -----------|     |     |--     |----|     ----|     |      |-----------
+#                   |                                  |
+#                  \|/                                \|/
+#                |------|                           |------|
+#                |divapp|                           |divapp|
+#                |------|                           |------|
+#
+# The basic cases:
+#   - inbound > diverted               | divapp terminated
+#   - inbound > diverted > inbound     | host terminated
+#   - inbound > diverted > outbound    | network terminated
+#   - outbound > diverted              | divapp terminated
+#   - outbound > diverted > outbound   | network terminated
+#   - outbound > diverted > inbound    | e.g. host terminated
+#
+# When a packet is diverted, forwarded, and possibly diverted again:
+#   - inbound > diverted > inbound > forwarded
+#         > outbound                       | network terminated
+#   - inbound > diverted > inbound > forwarded
+#         > outbound > diverted > outbound | network terminated
+#
+# Test case naming legend:
+# in - inbound
+# div - diverted
+# out - outbound
+# fwd - forwarded
+# ipfwon - with ipfw enabled, which allows all
+#
+
+. $(atf_get_srcdir)/utils.subr
+
+divert_init()
+{
+	if ! kldstat -q -m ipdivert; then
+		atf_skip "This test requires ipdivert"
+	fi
+}
+
+ipfw_init()
+{
+	if ! kldstat -q -m ipfw; then
+		atf_skip "This test requires ipfw"
+	fi
+}
+
+assert_ipfw_is_off()
+{
+	if kldstat -q -m ipfw; then
+		atf_skip "This test is for the case when ipfw is not loaded"
+	fi
+}
+
+atf_test_case "ipfwoff_in_div" "cleanup"
+ipfwoff_in_div_head()
+{
+	atf_set descr 'Test inbound > diverted | divapp terminated'
+	atf_set require.user root
+}
+ipfwoff_in_div_body()
+{
+	local ipfwon
+
+	pft_init
+	divert_init
+	test "$1" == "ipfwon" && ipfwon="yes"
+	test $ipfwon && ipfw_init || assert_ipfw_is_off
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail div ${epair}b
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec div ifconfig ${epair}b 192.0.2.2/24 up
+	test $ipfwon && jexec div ipfw add 65534 allow all from any to any
+
+	# Sanity check
+	atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+	jexec div pfctl -e
+	pft_set_rules div \
+		"pass all" \
+		"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000"
+
+	jexec div $(atf_get_srcdir)/divapp 2000 &
+	divapp_pid=$!
+	# Wait for the divapp to be ready
+	sleep 1
+
+	# divapp is expected to "eat" the packet
+	atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2
+
+	wait $divapp_pid
+}
+ipfwoff_in_div_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwon_in_div" "cleanup"
+ipfwon_in_div_head()
+{
+	atf_set descr 'Test inbound > diverted | divapp terminated, with ipfw enabled'
+	atf_set require.user root
+}
+ipfwon_in_div_body()
+{
+	ipfwoff_in_div_body "ipfwon"
+}
+ipfwon_in_div_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwoff_in_div_in" "cleanup"
+ipfwoff_in_div_in_head()
+{
+	atf_set descr 'Test inbound > diverted > inbound | host terminated'
+	atf_set require.user root
+}
+ipfwoff_in_div_in_body()
+{
+	local ipfwon
+
+	pft_init
+	divert_init
+	test "$1" == "ipfwon" && ipfwon="yes"
+	test $ipfwon && ipfw_init || assert_ipfw_is_off
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail div ${epair}b
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec div ifconfig ${epair}b 192.0.2.2/24 up
+	test $ipfwon && jexec div ipfw add 65534 allow all from any to any
+
+	# Sanity check
+	atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+	jexec div pfctl -e
+	pft_set_rules div \
+		"pass all" \
+		"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2000 no state"
+
+	jexec div $(atf_get_srcdir)/divapp 2000 divert-back &
+	divapp_pid=$!
+	# Wait for the divapp to be ready
+	sleep 1
+
+	# divapp is NOT expected to "eat" the packet
+	atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+	wait $divapp_pid
+}
+ipfwoff_in_div_in_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwon_in_div_in" "cleanup"
+ipfwon_in_div_in_head()
+{
+	atf_set descr 'Test inbound > diverted > inbound | host terminated, with ipfw enabled'
+	atf_set require.user root
+}
+ipfwon_in_div_in_body()
+{
+	ipfwoff_in_div_in_body "ipfwon"
+}
+ipfwon_in_div_in_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwoff_out_div" "cleanup"
+ipfwoff_out_div_head()
+{
+	atf_set descr 'Test outbound > diverted | divapp terminated'
+	atf_set require.user root
+}
+ipfwoff_out_div_body()
+{
+	local ipfwon
+
+	pft_init
+	divert_init
+	test "$1" == "ipfwon" && ipfwon="yes"
+	test $ipfwon && ipfw_init || assert_ipfw_is_off
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail div ${epair}b
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec div ifconfig ${epair}b 192.0.2.2/24 up
+	test $ipfwon && jexec div ipfw add 65534 allow all from any to any
+
+	# Sanity check
+	atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+	jexec div pfctl -e
+	pft_set_rules div \
+		"pass all" \
+		"pass in inet proto icmp icmp-type echoreq no state" \
+		"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state"
+
+	jexec div $(atf_get_srcdir)/divapp 2000 &
+	divapp_pid=$!
+	# Wait for the divapp to be ready
+	sleep 1
+
+	# divapp is expected to "eat" the packet
+	atf_check -s not-exit:0 -o ignore ping -c1 192.0.2.2
+
+	wait $divapp_pid
+}
+ipfwoff_out_div_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwon_out_div" "cleanup"
+ipfwon_out_div_head()
+{
+	atf_set descr 'Test outbound > diverted | divapp terminated, with ipfw enabled'
+	atf_set require.user root
+}
+ipfwon_out_div_body()
+{
+	ipfwoff_out_div_body "ipfwon"
+}
+ipfwon_out_div_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwoff_out_div_out" "cleanup"
+ipfwoff_out_div_out_head()
+{
+	atf_set descr 'Test outbound > diverted > outbound | network terminated'
+	atf_set require.user root
+}
+ipfwoff_out_div_out_body()
+{
+	local ipfwon
+
+	pft_init
+	divert_init
+	test "$1" == "ipfwon" && ipfwon="yes"
+	test $ipfwon && ipfw_init || assert_ipfw_is_off
+
+	epair=$(vnet_mkepair)
+	vnet_mkjail div ${epair}b
+	ifconfig ${epair}a 192.0.2.1/24 up
+	jexec div ifconfig ${epair}b 192.0.2.2/24 up
+	test $ipfwon && jexec div ipfw add 65534 allow all from any to any
+
+	# Sanity check
+	atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+	jexec div pfctl -e
+	pft_set_rules div \
+		"pass all" \
+		"pass in inet proto icmp icmp-type echoreq no state" \
+		"pass out inet proto icmp icmp-type echorep divert-to 127.0.0.1 port 2000 no state"
+
+	jexec div $(atf_get_srcdir)/divapp 2000 divert-back &
+	divapp_pid=$!
+	# Wait for the divapp to be ready
+	sleep 1
+
+	# divapp is NOT expected to "eat" the packet
+	atf_check -s exit:0 -o ignore ping -c1 192.0.2.2
+
+	wait $divapp_pid
+}
+ipfwoff_out_div_out_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwon_out_div_out" "cleanup"
+ipfwon_out_div_out_head()
+{
+	atf_set descr 'Test outbound > diverted > outbound | network terminated, with ipfw enabled'
+	atf_set require.user root
+}
+ipfwon_out_div_out_body()
+{
+	ipfwoff_out_div_out_body "ipfwon"
+}
+ipfwon_out_div_out_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwoff_in_div_in_fwd_out_div_out" "cleanup"
+ipfwoff_in_div_in_fwd_out_div_out_head()
+{
+	atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated'
+	atf_set require.user root
+}
+ipfwoff_in_div_in_fwd_out_div_out_body()
+{
+	local ipfwon
+
+	pft_init
+	divert_init
+	test "$1" == "ipfwon" && ipfwon="yes"
+	test $ipfwon && ipfw_init || assert_ipfw_is_off
+
+	# host <a--epair0--b> router <a--epair1--b> site
+	epair0=$(vnet_mkepair)
+	epair1=$(vnet_mkepair)
+
+	vnet_mkjail router ${epair0}b ${epair1}a
+	ifconfig ${epair0}a 192.0.2.1/24 up
+	jexec router sysctl net.inet.ip.forwarding=1
+	jexec router ifconfig ${epair0}b 192.0.2.2/24 up
+	jexec router ifconfig ${epair1}a 198.51.100.1/24 up
+	test $ipfwon && jexec router ipfw add 65534 allow all from any to any
+
+	vnet_mkjail site ${epair1}b
+	jexec site ifconfig ${epair1}b 198.51.100.2/24 up
+	jexec site route add default 198.51.100.1
+	test $ipfwon && jexec site ipfw add 65534 allow all from any to any
+
+	route add -net 198.51.100.0/24 192.0.2.2
+
+	# Sanity check
+	atf_check -s exit:0 -o ignore ping -c3 192.0.2.2
+
+	# Should be routed without pf
+	atf_check -s exit:0 -o ignore ping -c3 198.51.100.2
+
+	jexec router pfctl -e
+	pft_set_rules router \
+		"pass all" \
+		"pass in inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2001 no state" \
+		"pass out inet proto icmp icmp-type echoreq divert-to 127.0.0.1 port 2002 no state"
+
+	jexec router $(atf_get_srcdir)/divapp 2001 divert-back &
+	indivapp_pid=$!
+	jexec router $(atf_get_srcdir)/divapp 2002 divert-back &
+	outdivapp_pid=$!
+	# Wait for the divappS to be ready
+	sleep 1
+
+	# Both divappS are NOT expected to "eat" the packet
+	atf_check -s exit:0 -o ignore ping -c1 198.51.100.2
+
+	wait $indivapp_pid && wait $outdivapp_pid
+}
+ipfwoff_in_div_in_fwd_out_div_out_cleanup()
+{
+	pft_cleanup
+}
+
+atf_test_case "ipfwon_in_div_in_fwd_out_div_out" "cleanup"
+ipfwon_in_div_in_fwd_out_div_out_head()
+{
+	atf_set descr 'Test inbound > diverted > inbound > forwarded > outbound > diverted > outbound | network terminated, with ipfw enabled'
+	atf_set require.user root
+}
+ipfwon_in_div_in_fwd_out_div_out_body()
+{
+	ipfwoff_in_div_in_fwd_out_div_out_body "ipfwon"
+}
+ipfwon_in_div_in_fwd_out_div_out_cleanup()
+{
+	pft_cleanup
+}
+
+atf_init_test_cases()
+{
+	atf_add_test_case "ipfwoff_in_div"
+	atf_add_test_case "ipfwoff_in_div_in"
+	atf_add_test_case "ipfwon_in_div"
+	atf_add_test_case "ipfwon_in_div_in"
+
+	atf_add_test_case "ipfwoff_out_div"
+	atf_add_test_case "ipfwoff_out_div_out"
+	atf_add_test_case "ipfwon_out_div"
+	atf_add_test_case "ipfwon_out_div_out"
+
+	atf_add_test_case "ipfwoff_in_div_in_fwd_out_div_out"
+	atf_add_test_case "ipfwon_in_div_in_fwd_out_div_out"
+}