Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F177548310
D61194.1794684032.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
11 KB
Referenced Files
None
Subscribers
None
D61194.1794684032.diff
View Options
diff --git a/share/man/man4/ipsec.4 b/share/man/man4/ipsec.4
--- a/share/man/man4/ipsec.4
+++ b/share/man/man4/ipsec.4
@@ -27,7 +27,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
-.Dd May 7, 2026
+.Dd November 4, 2026
.Dt IPSEC 4
.Os
.Sh NAME
@@ -240,6 +240,7 @@
.It "net.inet.ipsec.natt_cksum_policy integer yes"
.It "net.inet.ipsec.check_policy_history integer yes"
.It "net.inet.ipsec.random_id integer yes"
+.It "net.inet.ipsec.sa_flowid integer yes"
.It "net.inet6.ipsec6.ecn integer yes"
.It "net.inet6.ipsec6.debug integer yes"
.El
@@ -292,6 +293,27 @@
by the IKE daemon, the kernel incrementally recomputes checksums for
inbound TCP segments and UDP datagrams.
If addresses were not configured, the checksums are ignored.
+.It Li ipsec.sa_flowid
+If set to non-zero (the default), packets leaving the kernel's IPsec
+transforms carry a flow ID derived from the SPI of their security association
+instead of that of the inner traffic.
+An interface, a
+.Xr lagg 4
+with
+.Cm use_flowid
+or a multipath route that selects a transmit queue, port or path by flow ID
+then keeps the packets of one SA together, which avoids the reordering of
+their sequence numbers between independently draining queues.
+Concurrent processing and asynchronous crypto completion can still reorder
+them before they are queued.
+If set to 0, the packets keep the flow ID of the inner traffic, so one SA may
+spread over several queues and its sequence numbers interleave, which a peer
+with a small anti-replay window drops.
+The ID is set before the packet is handed to a network interface that
+implements the security association itself, so such SAs select their queue
+the same way.
+To spread a tunnel over several queues, ports or paths, use several SAs;
+distinct SAs may still select the same one.
.It Li ipsec.check_policy_history
Enables strict policy checking for inbound packets.
By default, inbound security policies check that packets handled by IPsec
diff --git a/sys/netipsec/ipsec.h b/sys/netipsec/ipsec.h
--- a/sys/netipsec/ipsec.h
+++ b/sys/netipsec/ipsec.h
@@ -297,6 +297,7 @@
VNET_DECLARE(int, ip4_ipsec_random_id);
VNET_DECLARE(int, crypto_support);
VNET_DECLARE(int, async_crypto);
+VNET_DECLARE(int, sa_flowid);
VNET_DECLARE(int, natt_cksum_policy);
#define IPSECSTAT_INC(name) \
@@ -315,6 +316,7 @@
#define V_ip4_ipsec_random_id VNET(ip4_ipsec_random_id)
#define V_crypto_support VNET(crypto_support)
#define V_async_crypto VNET(async_crypto)
+#define V_sa_flowid VNET(sa_flowid)
#define V_natt_cksum_policy VNET(natt_cksum_policy)
#define ipseclog(x) do { if (V_ipsec_debug) log x; } while (0)
diff --git a/sys/netipsec/ipsec.c b/sys/netipsec/ipsec.c
--- a/sys/netipsec/ipsec.c
+++ b/sys/netipsec/ipsec.c
@@ -159,6 +159,8 @@
* 1 - enabled
*/
VNET_DEFINE(int, async_crypto) = 0;
+/* Derive the flow ID of outbound packets from the SA: one SA, one queue. */
+VNET_DEFINE(int, sa_flowid) = 1;
/*
* TCP/UDP checksum handling policy for transport mode NAT-T (RFC3948)
@@ -211,6 +213,9 @@
SYSCTL_INT(_net_inet_ipsec, OID_AUTO, async_crypto,
CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(async_crypto), 0,
"Use asynchronous mode to parallelize crypto jobs.");
+SYSCTL_INT(_net_inet_ipsec, OID_AUTO, sa_flowid,
+ CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(sa_flowid), 0,
+ "Derive the flow ID of outbound packets from the SA.");
SYSCTL_INT(_net_inet_ipsec, OID_AUTO, check_policy_history,
CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(check_policy_history), 0,
"Use strict check of inbound packets to security policy compliance.");
diff --git a/sys/netipsec/ipsec_output.c b/sys/netipsec/ipsec_output.c
--- a/sys/netipsec/ipsec_output.c
+++ b/sys/netipsec/ipsec_output.c
@@ -109,6 +109,22 @@
static int ipsec_encap(struct mbuf **mp, struct secasindex *saidx);
static size_t ipsec_get_pmtu(struct secasvar *sav);
+/*
+ * Select the transmit queue by SA: its packets then do not reorder between
+ * independently draining queues. Done before the transforms, software or
+ * offloaded, so that the SA decides the queue either way; in a bundle the
+ * last SA decides. Concurrent processing and crypto completions can still
+ * reorder the packets before they are queued.
+ */
+static void
+ipsec_set_flowid(struct mbuf *m, struct secasvar *sav)
+{
+ if (V_sa_flowid) {
+ m->m_pkthdr.flowid = sav->flowid;
+ M_HASHTYPE_SET(m, M_HASHTYPE_OPAQUE_HASH);
+ }
+}
+
#ifdef INET
static struct secasvar *
ipsec4_allocsa(struct ifnet *ifp, struct mbuf *m, const struct ip *ip,
@@ -227,6 +243,7 @@
/* Re-calculate *ip1 after potential change of m in the hook. */
m_copydata(m, 0, sizeof(*ip1), (char *)ip1);
+ ipsec_set_flowid(m, sav);
hwassist = 0;
accel = ipsec_accel_output(ifp, m, inp, sp, sav, AF_INET, mtu,
&hwassist);
@@ -656,6 +673,7 @@
if ((error = ipsec_run_hhooks(&ctx, HHOOK_TYPE_IPSEC_OUT)) != 0)
goto bad;
+ ipsec_set_flowid(m, sav);
hwassist = 0;
accel = ipsec_accel_output(ifp, m, inp, sp, sav, AF_INET6, mtu,
&hwassist);
diff --git a/sys/netipsec/key.c b/sys/netipsec/key.c
--- a/sys/netipsec/key.c
+++ b/sys/netipsec/key.c
@@ -42,6 +42,7 @@
#include <sys/types.h>
#include <sys/param.h>
#include <sys/systm.h>
+#include <sys/hash.h>
#include <sys/kernel.h>
#include <sys/fnv_hash.h>
#include <sys/lock.h>
@@ -3083,6 +3084,9 @@
}
sav->spi = spi;
+ /* Hash the SPI in host order: the same flow ID on every architecture. */
+ spi = ntohl(spi);
+ sav->flowid = jenkins_hash32(&spi, 1, 0);
sav->seq = mhp->msg->sadb_msg_seq;
sav->state = SADB_SASTATE_LARVAL;
sav->pid = (pid_t)mhp->msg->sadb_msg_pid;
diff --git a/sys/netipsec/keydb.h b/sys/netipsec/keydb.h
--- a/sys/netipsec/keydb.h
+++ b/sys/netipsec/keydb.h
@@ -150,6 +150,7 @@
*/
struct secasvar {
uint32_t spi; /* SPI Value, network byte order */
+ uint32_t flowid; /* flow ID of its packets, from the SPI */
uint32_t flags; /* holder for SADB_KEY_FLAGS */
uint32_t seq; /* sequence number */
pid_t pid; /* message's pid */
diff --git a/tests/sys/netipsec/tunnel/Makefile b/tests/sys/netipsec/tunnel/Makefile
--- a/tests/sys/netipsec/tunnel/Makefile
+++ b/tests/sys/netipsec/tunnel/Makefile
@@ -11,7 +11,8 @@
aesni_aes_cbc_256_hmac_sha2_256 \
aesni_aes_gcm_128 \
aesni_aes_gcm_256 \
- chacha20_poly1305
+ chacha20_poly1305 \
+ sa_flowid
TEST_METADATA+= execenv="jail" \
execenv_jail_params="vnet allow.raw_sockets"
diff --git a/tests/sys/netipsec/tunnel/sa_flowid.sh b/tests/sys/netipsec/tunnel/sa_flowid.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/netipsec/tunnel/sa_flowid.sh
@@ -0,0 +1,165 @@
+
+. $(atf_get_srcdir)/utils.subr
+
+# Outbound packets of a tunnel mode SA carry the SA's flow ID,
+# jenkins_hash32() of the SPI in host byte order with seed 0, in place of
+# the inner packet's. A lagg(4) with use_flowid picks its port from
+# (flowid >> flowid_shift) % ports, so with two ports and the default shift
+# of 16 the SAs below group as
+# 0x1000 (a09625cd) and 0x1001 (e1fead43): bit 16 clear
+# 0x1003 (a7bb1c69) and 0x1004 (1cdb9da5): bit 16 set
+# while lagg's own header hash, restricted to layer 2, puts every SA on the
+# same port.
+#
+# Each SA carries NFLOW inner flows, 172.16.K.1 .. 172.16.K.NFLOW to
+# 172.16.K.254, which have flow IDs of their own: the kernel hashes locally
+# generated packets once a multipath route exists. The hash key is a
+# constant, so the inner flows of every SA spread over both ports whenever
+# their flow IDs reach the lagg.
+NFLOW=8
+KEY="12345678901234567890"
+
+sf_setup()
+{
+ ist_init
+ _vnet_check_req lagg
+ epair0=$(vnet_mkepair)
+ epair1=$(vnet_mkepair)
+ vnet_mkjail a ${epair0}a ${epair1}a
+ vnet_mkjail b ${epair0}b ${epair1}b
+ for j in a b; do
+ [ ${j} = a ] && s=a || s=b
+ jexec ${j} ifconfig ${epair0}${s} up
+ jexec ${j} ifconfig ${epair1}${s} up
+ jexec ${j} ifconfig lagg0 create
+ jexec ${j} ifconfig lagg0 laggproto loadbalance \
+ laggport ${epair0}${s} laggport ${epair1}${s}
+ jexec ${j} ifconfig lo0 up
+ done
+ jexec a ifconfig lagg0 inet 10.0.0.1/24 up
+ jexec b ifconfig lagg0 inet 10.0.0.2/24 up
+ for k in 0 1 3 4; do
+ n=1
+ while [ ${n} -le ${NFLOW} ]; do
+ jexec a ifconfig lo0 alias 172.16.${k}.${n}/32
+ n=$((n + 1))
+ done
+ jexec b ifconfig lo0 alias 172.16.${k}.254/32
+ done
+ jexec a route -q add -net 172.16.0.0/16 10.0.0.2
+ jexec b route -q add -net 172.16.0.0/16 10.0.0.1
+ # Give the inner packets flow IDs: net.route.hash_outbound is read-only
+ # and turns on with the first multipath route. This one carries nothing.
+ jexec a route -q add -net 192.0.2.0/24 10.0.0.2
+ jexec a route -q add -net 192.0.2.0/24 10.0.0.3
+ if [ "$(jexec a sysctl -n net.route.hash_outbound)" != 1 ]; then
+ atf_skip "This test requires net.route.hash_outbound"
+ fi
+ for j in a b; do
+ [ ${j} = a ] && dir=out || dir=in
+ [ ${j} = a ] && rdir=in || rdir=out
+ (
+ printf "flush;\nspdflush;\n"
+ for k in 0 1 3 4; do
+ spi=$((0x1000 + k)); rspi=$((0x2000 + k))
+ printf "add 10.0.0.1 10.0.0.2 esp ${spi} -m tunnel -u ${spi} -E aes-gcm-16 \"${KEY}\";\n"
+ printf "add 10.0.0.2 10.0.0.1 esp ${rspi} -m tunnel -u ${rspi} -E aes-gcm-16 \"${KEY}\";\n"
+ printf "spdadd 172.16.${k}.0/24 172.16.${k}.254/32 any -P ${dir} ipsec esp/tunnel/10.0.0.1-10.0.0.2/unique:${spi};\n"
+ printf "spdadd 172.16.${k}.254/32 172.16.${k}.0/24 any -P ${rdir} ipsec esp/tunnel/10.0.0.2-10.0.0.1/unique:${rspi};\n"
+ done
+ ) > ${TMPDIR}/ipsec.${j}.conf
+ atf_check -s exit:0 -o ignore jexec ${j} setkey -f ${TMPDIR}/ipsec.${j}.conf
+ done
+ # Resolve ARP and bring the SAs up before counting.
+ atf_check -s exit:0 -o ignore jexec a ping -c 1 10.0.0.2
+ for k in 0 1 3 4; do
+ atf_check -s exit:0 -o ignore jexec a ping -c 1 -S 172.16.${k}.1 172.16.${k}.254
+ done
+ # Header hashing alone (layer 2 only) cannot tell the SAs apart.
+ jexec a ifconfig lagg0 use_flowid lagghash l2
+}
+
+# sf_opackets IF: packets jail a has sent on IF
+sf_opackets()
+{
+ jexec a netstat -ibn -I $1 | awk '$3 ~ /^<Link/ { print $9 }'
+}
+
+# sf_send K: ping over every inner flow of SA 0x100K, 10 packets each, and
+# set d0 and d1 to the packets that left on lagg port 0 and 1. Not a
+# command substitution: atf_fail must end the test.
+sf_send()
+{
+ b0=$(sf_opackets ${epair0}a); b1=$(sf_opackets ${epair1}a)
+ n=1
+ while [ ${n} -le ${NFLOW} ]; do
+ jexec a ping -q -c 10 -i 0.02 -S 172.16.$1.${n} 172.16.$1.254 \
+ > /dev/null 2>&1 ||
+ atf_fail "ping from 172.16.$1.${n} over SA 0x100$1 failed"
+ n=$((n + 1))
+ done
+ d0=$(( $(sf_opackets ${epair0}a) - b0 )); d1=$(( $(sf_opackets ${epair1}a) - b1 ))
+ echo "SA 0x100$1: ${d0} packets on port 0, ${d1} on port 1"
+}
+
+# sf_member K VAR: set VAR to the lagg port (0 or 1) that carries every
+# inner flow of SA 0x100K.
+sf_member()
+{
+ sf_send $1
+ if [ ${d0} -ge $((10 * NFLOW)) ] && [ ${d1} -le 2 ]; then
+ eval $2=0
+ elif [ ${d1} -ge $((10 * NFLOW)) ] && [ ${d0} -le 2 ]; then
+ eval $2=1
+ else
+ atf_fail "SA 0x100$1 did not use one port for all of its flows"
+ fi
+}
+
+atf_test_case "select" "cleanup"
+select_head()
+{
+ atf_set descr 'lagg(4) use_flowid selects the port of an SA from its flow ID'
+ atf_set require.user root
+}
+select_body()
+{
+ sf_setup
+ sf_member 0 m0; sf_member 1 m1; sf_member 3 m3; sf_member 4 m4
+ echo "ports: 0x1000 -> ${m0}, 0x1001 -> ${m1}, 0x1003 -> ${m3}, 0x1004 -> ${m4}"
+ if [ "${m0}" != "${m1}" ] || [ "${m3}" != "${m4}" ] || [ "${m0}" = "${m3}" ]; then
+ atf_fail "SAs did not group by bit 16 of their flow ID"
+ fi
+}
+select_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_test_case "off" "cleanup"
+off_head()
+{
+ atf_set descr 'With net.inet.ipsec.sa_flowid=0 packets keep the inner flow ID'
+ atf_set require.user root
+}
+off_body()
+{
+ sf_setup
+ atf_check -s exit:0 -o ignore jexec a sysctl net.inet.ipsec.sa_flowid=0
+ for k in 0 1 3 4; do
+ sf_send ${k}
+ if [ ${d0} -lt 10 ] || [ ${d1} -lt 10 ]; then
+ atf_fail "the inner flows of SA 0x100${k} did not spread over the ports"
+ fi
+ done
+}
+off_cleanup()
+{
+ vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case "select"
+ atf_add_test_case "off"
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Nov 14, 7:20 PM (5 h, 6 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41690255
Default Alt Text
D61194.1794684032.diff (11 KB)
Attached To
Mode
D61194: netipsec: Select outbound transmit queues by SA instead of inner flow
Attached
Detach File
Event Timeline