lists.openwall.net   lists  /  announce  owl-users  owl-dev  john-users  john-dev  passwdqc-users  yescrypt  popa3d-users  /  oss-security  kernel-hardening  musl  sabotage  tlsify  passwords  /  crypt-dev  xvendor  /  Bugtraq  Full-Disclosure  linux-kernel  linux-netdev  linux-ext4  linux-hardening  linux-cve-announce  PHC 
Open Source and information security mailing list archives
 
Hash Suite: Windows password security audit tool. GUI, reports in PDF.
[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <20260203004853.94438-11-tom@herbertland.com>
Date: Mon,  2 Feb 2026 16:48:53 -0800
From: Tom Herbert <tom@...bertland.com>
To: davem@...emloft.net,
	kuba@...nel.org,
	netdev@...r.kernel.org,
	justin.iurman@...ege.be,
	willemdebruijn.kernel@...il.com
Cc: Tom Herbert <tom@...bertland.com>
Subject: [PATCH net-next v6 10/10] test: Add networking sefltest for eh limits

Add a networking selftest for Extension Header limits. The
limits to test are in systcls:

	net.ipv6.enforce_ext_hdr_order
	net.ipv6.max_dst_opts_number
	net.ipv6.max_hbh_opts_number
	net.ipv6.max_hbh_length
	net.ipv6.max_dst_opts_length

The basic idea of the test is to fabricate ICMPv6 Echo Request
packets with various combinations of Extension Headers. The packets
are sent to a host in another namespace. If a an ICMPv6 Echo Reply
is received then the packet wasn't dropped due to a limit being
exceeded, and if it was dropped then we assume that a limit was
exceeded. For each test packet we derive an expectation as to
whether the packet will be dropped or not. Test success depends
on whether our expectation is matched. i.e. if we expect a reply
then the test succeeds if we see a reply, and if we don't expect a
reply then the test succeeds if we don't see a reply.

The test is divided into a frontend bash script (eh_limits.sh) and a
backend Python script (eh_limits.py).

The frontend sets up two network namespaces with IPv6 addresses
configured on veth's. We then invoke the backend to send the
test packets. This first pass is done with default sysctl settings.
On a second pass we change the various sysctl settings and run
again.

The backend runs through the various test cases described in the
Make_Test_Packets function. This function calls Make_Packet for
a test case where arguments provide the Extension Header chain to
be tested. The Run_Test function loops through the various packets
and tests if a reply is received versus the expectation. If a test
case fails then an error status is returned by the backend.

The backend script can also be run with the "-w <pcap_file>" to
write the created packets to a pcap file instead of running the
test.

Signed-off-by: Tom Herbert <tom@...bertland.com>
---
 tools/testing/selftests/net/Makefile     |   1 +
 tools/testing/selftests/net/eh_limits.py | 352 +++++++++++++++++++++++
 tools/testing/selftests/net/eh_limits.sh | 205 +++++++++++++
 3 files changed, 558 insertions(+)
 create mode 100755 tools/testing/selftests/net/eh_limits.py
 create mode 100755 tools/testing/selftests/net/eh_limits.sh

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index afdea6d95bde..73195a2e8f1e 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -24,6 +24,7 @@ TEST_PROGS := \
 	cmsg_time.sh \
 	double_udp_encap.sh \
 	drop_monitor_tests.sh \
+	eh_limits.sh \
 	fcnal-ipv4.sh \
 	fcnal-ipv6.sh \
 	fcnal-other.sh \
diff --git a/tools/testing/selftests/net/eh_limits.py b/tools/testing/selftests/net/eh_limits.py
new file mode 100755
index 000000000000..01fcf15c6b94
--- /dev/null
+++ b/tools/testing/selftests/net/eh_limits.py
@@ -0,0 +1,352 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+
+# Test of extension header limits
+
+import ext_hdr
+import getopt
+import proto_nums
+import shlex
+
+from scapy.all import *
+
+# Constants
+
+verbose = False
+source_mac = "00:11:22:33:44:55"
+destination_mac = "AA:BB:CC:DD:EE:FF"
+source_ip = "2001:db8::7"
+destination_ip = "2001:db8::8"
+packet_list = []
+pcap_out=""
+glob_ident = 1111
+ether_frame = Raw()
+with_eth = False
+
+# Parse command line options
+def Cli_Args():
+	global verbose, source_mac, destination_mac, source_ip
+	global destination_ip, pcap_out
+
+	args = sys.argv[1:]
+
+	try:
+		opts, remainder = getopt.getopt(args, "vw:",
+			["verbose", "src_eth=", "dst_eth", "src_ip=",
+			 "dst_ip=", "pcap_out="])
+	except getopt.GetoptError as err:
+		# Print error message and exit
+		print(err)
+		sys.exit(2)
+
+	for opt, arg in opts:
+		if opt in ("-v", "--verbose"):
+			verbose = True
+		elif opt in ("--src_eth"):
+			source_mac = arg
+		elif opt in ("--dst_eth"):
+			destination_mac = arg
+		elif opt in ("--src_ip"):
+			source_ip = arg
+		elif opt in ("--dst_ip"):
+			destination_ip = arg
+		elif opt in ("-w", "--pcap_out"):
+			pcap_out = arg
+
+# Make an ICMP echo request packet with the requested Extension Header chain
+def Make_Packet(text_name, eh_list):
+	global glob_ident,  packet_list, with_eth
+
+	hdr = Raw()
+	len = 0
+
+	hdr = ICMPv6EchoRequest(id=glob_ident)/hdr
+	len += 8
+
+	pair = ext_hdr.Make_EH_Chain(
+		proto_nums.IP_Proto.IP_PROTO_IPv6_ICMP.value, eh_list)
+	hdr = pair[0]/hdr
+	len += pair[1]
+
+	ipv6_pkt = IPv6(src=source_ip, dst=destination_ip, nh=pair[2], plen=len)
+	hdr = ipv6_pkt / hdr
+	len += 40
+
+	if (with_eth):
+		hdr = ether_frame/hdr
+		len += 14
+
+	packet_list.append((hdr, len, glob_ident, pair[3], text_name))
+	glob_ident += 1
+
+# Write a pacp file with all the created packets
+def Write_Pcap(pcap_out):
+	global packet_list, ether_frame, with_eth
+
+	ether_frame = Ether(src=source_mac, dst=destination_mac, type=0x86DD)
+	with_eth = True
+
+	packets=[]
+	for packet in packet_list:
+		packets.append(packet[0])
+	wrpcap(pcap_out, packets)
+
+# Run ping test
+def Run_Test():
+	global packet_list, verbose, source_ip, destination_ip
+
+	# Open raw ICMP socket
+	try:
+	    s = socket.socket(socket.AF_INET6, socket.SOCK_RAW,
+			      socket.IPPROTO_ICMPV6)
+	except PermissionError:
+		print("This script requires root privileges.")
+		return 2
+
+	# Bind to interface by its IP address
+	s.bind((source_ip, 0))
+
+	# Run through eACH PACKET
+	for packet in packet_list:
+		# Send packet
+		send(packet[0], verbose=False)
+
+		s.settimeout(0.100)
+		found_it = False
+
+		# Try to get ICMP echo reply
+		try:
+			while (found_it == False):
+				rpacket, addr = s.recvfrom(1024)
+				x = struct.unpack(">H", rpacket[0:2])
+				icmp_type, icmp_code = struct.unpack("BB",
+								rpacket[0:2])
+				checksum = struct.unpack("H", rpacket[2:4])
+				identifier = struct.unpack(">H", rpacket[4:6])
+				seqno = struct.unpack(">H", rpacket[6:8])
+				if (icmp_type ==
+				    proto_nums.ICMP6_Type.ICMPV6_ECHO_REPLY.value and
+				    identifier[0] == packet[2]):
+					found_it = True
+
+		except socket.timeout:
+			pass
+
+		if (verbose):
+			if (found_it):
+				if (packet[3]):
+					print("TEST: %s: Received as expected" %
+								packet[4])
+				else:
+					print("TEST: %s: Unexpedted receive" %
+								packet[4])
+			else:
+				if (packet[3]):
+					print("TEST: %s: Didn't receive, "
+					      "but receive expected" %
+								packet[4])
+				else:
+					print("TEST: %s: Didn't receive as "
+					      "expected" % packet[4])
+
+
+		if (found_it and packet[3] != True):
+			# We got a reply but weren't expecting one
+			print("FAIL: Receive was unexpected for %s" % packet[4])
+			return 1
+		elif (not found_it and packet[3]):
+			# We didn't get a reply but weret expecting one
+			print("FAIL: Expected to receive for %s" % packet[4])
+			return 1
+
+	return 0
+
+# Make packets for various test cases
+def Make_Test_Packets():
+	global packet_list
+
+	# Two non-padding options in HBH and DestOpt, should succeed
+	# with default sysctls
+	Make_Packet("Two non-padding options in HBH and DestOpts",
+	    [
+		("H", [(11, 4), (0, 0), (0, 0), (12, 3)]),
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3)]),
+	    ])
+
+	# Big destination option, should fail when
+	# net.ipv6.max_dst_opts_length equals 64
+	Make_Packet("Big destination option",
+	    [
+		("H", [(11, 4), (0, 0), (0, 0), (12, 3)]),
+		("D", [(1, 4), (12, 255)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+	    ])
+
+	# Almost Big HBH option should succeed when
+	# net.ipv6.max_hbh_length equals 64
+	Make_Packet("Almost Big HBH option",
+	    [
+		("H", [(11, 53), (1, 0), (12, 3)]),
+		("D", [(1, 4), (12, 1)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+	    ])
+
+	# Big Hop-by-Hop option, should fail when
+	# net.ipv6.max_hbh_length equals 64
+	Make_Packet("Big HBH option",
+	    [
+		("H", [(11, 53), (1, 0), (0, 0), (12, 3)]),
+		("D", [(1, 4), (12, 1)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+	    ])
+
+	# Too much HBH padding, should always fail
+	Make_Packet("Too much HBH padding",
+	    [
+		("H", [(12, 3), (1, 8)]),
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3)]),
+	    ])
+
+	# Too much DestOpt padding, should always fail
+	Make_Packet("Too much DestOpt padding",
+	    [
+		("H", [(12, 3), (1, 8)]),
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3), (0, 0), (1, 6), (0, 0), (12, 3)]),
+	    ])
+
+	# Too much DestOpt padding, should always fail
+	Make_Packet("Too much DestOpt padding #2",
+	    [
+		("H", [(12, 3)]),
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3), (0, 0), (0, 0), (0, 0), (0, 0),
+		       (0, 0), (0, 0), (0, 0), (0, 0), (12, 3)]),
+	    ])
+
+	# Too much DestOpt padding, should always fail
+
+	Make_Packet("Too much DestOpt padding #3",
+	    [
+		("D", [(0, 0), (0, 0), (0, 0), (0, 0),
+		       (0, 0), (0, 0), (0, 0), (0, 0)]),
+	    ])
+
+	# Almost too much DestOpt padding, should succeed with default
+	# sysctl settings
+	Make_Packet("Almost too much DestOpt padding #2",
+	    [
+		("H", [(12, 3)]),
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3), (0, 0), (0, 0), (0, 0), (0, 0),
+		       (0, 0), (0, 0), (0, 0), (12, 3)]),
+	    ])
+
+	# Two Dest Ops, should fail unless net.ipv6.enforce_ext_hdr_order
+	# equals 1
+	Make_Packet("Two Dest Ops",
+	    [
+		("D", []),
+		("D", []),
+	    ])
+
+	# OOO Routing headers, should fail unless
+	# net.ipv6.enforce_ext_hdr_order equals 1
+	Make_Packet("OOO Routing header",
+	    [
+		("F", (0x89abcdef)),
+		("R", [ "888::1", "9999::1"]),
+	    ])
+
+	# Two Routing headers, should fail unless
+	# net.ipv6.enforce_ext_hdr_order equals 1
+	Make_Packet("Two Routing headers",
+	    [
+		("R", [ "888::1", "9999::1"]),
+		("R", [ "888::1", "9999::1"]),
+	    ])
+
+	# Two DestOpt headers with Routing header should succeed with default
+	# sysctl settings
+	Make_Packet("Two Destination options okay",
+	    [
+		("D", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3)]),
+	    ])
+
+	# Two DestOpt headers without Routing header should fail unless
+	# net.ipv6.enforce_ext_hdr_order equals 1
+	Make_Packet("Two Destination options",
+	    [
+		("D", [(1, 4), (12, 3)]),
+		("D", [(1, 4), (12, 3)]),
+	    ])
+
+	# Two DestOpt headers after Routing header, should fail unless
+	# net.ipv6.enforce_ext_hdr_order equals 1
+	Make_Packet("Two Destination options after RH",
+	    [
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3)]),
+		("D", [(1, 4), (12, 3)]),
+	    ])
+
+	# Many extension headers, should fail unless
+	# net.ipv6.enforce_ext_hdr_order equals 1
+	Make_Packet("Many EH OOO",
+	    [
+		("H", [(1, 4), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3)]),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3)]),
+		("F", (0x89abcdef)),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+		("R", [ "888::1", "9999::1"]),
+		("R", [ "888::1", "9999::1"]),
+		("D", [(1, 4), (12, 3), (12, 3)]),
+	    ])
+
+	# Two fragment headers, should always fail due to stack
+	# implementation
+	Make_Packet("Two fragment Headers",
+	    [
+		("F", (0x89abcdef)),
+		("F", (0x89abcdef)),
+	    ])
+
+Cli_Args()
+
+Make_Test_Packets()
+
+if (pcap_out != ""):
+	Write_Pcap(pcap_out)
+	status = 0
+else:
+	status = Run_Test()
+
+exit(status)
diff --git a/tools/testing/selftests/net/eh_limits.sh b/tools/testing/selftests/net/eh_limits.sh
new file mode 100755
index 000000000000..4a101930ab54
--- /dev/null
+++ b/tools/testing/selftests/net/eh_limits.sh
@@ -0,0 +1,205 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# Tests extension header limits.
+#
+# We start by setting up two network namespaces with IPv6 addresses.
+# Then we create ICMPv6 Echo Request packets with various combinations of
+# Extension Headers, and send them from one namespace to the other and
+# check if a reply is received. Based on the sysctl settings certain packets
+# are expected to produce echo replies and others are expected to be drop
+# because an Extension Header related limit is exceeded. If an Echo Reply is
+# received or not received per our expectations then the test passes,
+# otherwise if the result is unexpected that's a test failure.
+# Tests extension header limits.
+
+source lib.sh
+
+ret=0
+
+# all tests in this script. Can be overridden with -t option
+TESTS="eh_limits"
+
+VERBOSE=""
+PAUSE_ON_FAIL=no
+PAUSE=no
+NAME="EH-limits"
+
+IP1="2001:db8::1"
+IP2="2001:db8::2"
+
+log_test()
+{
+	local rc=$1
+	local expected=$2
+	local msg="$3"
+
+	if [ ${rc} -eq ${expected} ]; then
+		printf "    TEST: %-60s  [ OK ]\n" "${msg}"
+		nsuccess=$((nsuccess+1))
+	else
+		ret=1
+		nfail=$((nfail+1))
+		printf "    TEST: %-60s  [FAIL]\n" "${msg}"
+		if [ "${PAUSE_ON_FAIL}" = "yes" ]; then
+		echo
+			echo "hit enter to continue, 'q' to quit"
+			read a
+			[ "$a" = "q" ] && exit 1
+		fi
+	fi
+
+	if [ "${PAUSE}" = "yes" ]; then
+		echo
+		echo "hit enter to continue, 'q' to quit"
+		read a
+		[ "$a" = "q" ] && exit 1
+	fi
+}
+
+################################################################################
+# Setup
+
+setup()
+{
+	set -e
+
+	setup_ns ns1 ns2
+
+	NS_EXEC="ip netns exec"
+
+	ip link add veth1 type veth peer name veth2
+
+	ip link set veth1 netns "$ns1"
+	ip link set veth2 netns "$ns2"
+
+	$NS_EXEC "$ns1" ip addr add ${IP1}/64 dev veth1
+	$NS_EXEC "$ns1" ip link set veth1 up
+	$NS_EXEC "$ns1" ip link set lo up
+
+	$NS_EXEC "$ns2" ip addr add ${IP2}/64 dev veth2
+	$NS_EXEC "$ns2" ip link set veth2 up
+	$NS_EXEC "$ns2" ip link set lo up
+
+	# Enable SRv6 on the receiver since that's the type of routing header
+	# used in the test
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.conf.all.seg6_enabled=1 > /dev/null
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.conf.veth2.seg6_enabled=1 > /dev/null
+
+	set +e
+
+	# Send a ping to do neighuor discovery
+	$NS_EXEC "$ns1" ping6 -w 2 $IP2 -c 1 > /dev/null
+}
+
+exit_cleanup_all()
+{
+	cleanup_all_ns
+	exit "${EXIT_STATUS}"
+}
+
+eh_limits_test()
+{
+	local ip_addrs="--src_ip $IP1 --dst_ip $IP2"
+
+	if [ "$VERBOSE" = "-v" ]; then
+		echo ">>>>> Default"
+	fi
+
+	# Run the test with default sysctl settings
+	$NS_EXEC "$ns1" python3 ./eh_limits.py $VERBOSE $ip_addrs
+
+	log_test $? 0 "$NAME - default sysctls"
+
+	if [ "$VERBOSE" = "-v" ]; then
+		echo ">>>>> No order enforce, 8 options, 66 length limit"
+	fi
+
+	# Set extension header limit sysctls. We do this on both sides since
+	# the sender reads the sysctl's to determine pass/fail expectations
+
+	$NS_EXEC "$ns1" sysctl -w net.ipv6.enforce_ext_hdr_order=0 > /dev/null
+	$NS_EXEC "$ns1" sysctl -w net.ipv6.max_dst_opts_number=8 > /dev/null
+	$NS_EXEC "$ns1" sysctl -w net.ipv6.max_hbh_opts_number=8 > /dev/null
+	$NS_EXEC "$ns1" sysctl -w net.ipv6.max_hbh_length=64 > /dev/null
+	$NS_EXEC "$ns1" sysctl -w net.ipv6.max_dst_opts_length=64 > /dev/null
+
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.enforce_ext_hdr_order=0 > /dev/null
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.max_dst_opts_number=8 > /dev/null
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.max_hbh_opts_number=8 > /dev/null
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.max_hbh_length=64 > /dev/null
+	$NS_EXEC "$ns2" sysctl -w net.ipv6.max_dst_opts_length=64 > /dev/null
+
+	# Run the test with modified sysctl settings
+	$NS_EXEC "$ns1" python3 ./eh_limits.py $VERBOSE $ip_addrs
+
+	log_test $? 0 "$NAME - modified sysctls"
+}
+
+################################################################################
+# usage
+
+usage()
+{
+	cat <<EOF
+usage: ${0##*/} OPTS
+
+        -t <test>   Test(s) to run (default: all)
+                    (options: $TESTS)
+        -p          Pause on fail
+        -P          Pause after each test before cleanup
+        -v          verbose mode (show commands and output)
+EOF
+}
+
+################################################################################
+# main
+
+while getopts :t:pPhv o
+do
+	case $o in
+		t) TESTS=$OPTARG;;
+		p) PAUSE_ON_FAIL=yes;;
+		P) PAUSE=yes;;
+		v) VERBOSE="-v";;
+		h) usage; exit 0;;
+		*) usage; exit 1;;
+	esac
+done
+
+PEER_CMD="ip netns exec ${PEER_NS}"
+
+# make sure we don't pause twice
+[ "${PAUSE}" = "yes" ] && PAUSE_ON_FAIL=no
+
+if [ "$(id -u)" -ne 0 ];then
+	echo "SKIP: Need root privileges"
+	exit $ksft_skip;
+fi
+
+if [ ! -x "$(command -v ip)" ]; then
+	echo "SKIP: Could not run test without ip tool"
+	exit $ksft_skip
+fi
+
+if [ ! -x "$(command -v socat)" ]; then
+	echo "SKIP: Could not run test without socat tool"
+	exit $ksft_skip
+fi
+
+# start clean
+cleanup &> /dev/null
+
+for t in $TESTS
+do
+	case $t in
+	eh_limits)		setup; eh_limits_test; cleanup_all_ns;;
+
+	help) echo "Test names: $TESTS"; exit 0;;
+	esac
+done
+
+if [ "$TESTS" != "none" ]; then
+	printf "\nTests passed: %3d\n" ${nsuccess}
+	printf "Tests failed: %3d\n"   ${nfail}
+fi
-- 
2.43.0


Powered by blists - more mailing lists

Powered by Openwall GNU/*/Linux Powered by OpenVZ