diff --git a/tools/testing/selftests/net/forwarding/Makefile b/tools/testing/selftests/net/forwarding/Makefile
index 99576d7ecbf63..224346426ef22 100644
--- a/tools/testing/selftests/net/forwarding/Makefile
+++ b/tools/testing/selftests/net/forwarding/Makefile
@@ -39,6 +39,7 @@ TEST_PROGS = bridge_fdb_learning_limit.sh \
 	ipip_hier_gre.sh \
 	lib_sh_test.sh \
 	local_termination.sh \
+	min_max_mtu.sh \
 	mirror_gre_bound.sh \
 	mirror_gre_bridge_1d.sh \
 	mirror_gre_bridge_1d_vlan.sh \
diff --git a/tools/testing/selftests/net/forwarding/min_max_mtu.sh b/tools/testing/selftests/net/forwarding/min_max_mtu.sh
new file mode 100755
index 0000000000000..97bb8b221bed8
--- /dev/null
+++ b/tools/testing/selftests/net/forwarding/min_max_mtu.sh
@@ -0,0 +1,283 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# +--------------------+
+# | H1                 |
+# |                    |
+# |           $h1.10 + |
+# |     192.0.2.2/24 | |
+# | 2001:db8:1::2/64 | |
+# |                  | |
+# |              $h1 + |
+# |                  | |
+# +------------------|-+
+#                    |
+# +------------------|-+
+# | SW               | |
+# |            $swp1 + |
+# |                  | |
+# |         $swp1.10 + |
+# |     192.0.2.1/24   |
+# | 2001:db8:1::1/64   |
+# |                    |
+# +--------------------+
+
+ALL_TESTS="
+	ping_ipv4
+	ping_ipv6
+	max_mtu_config_test
+	max_mtu_traffic_test
+	min_mtu_config_test
+	min_mtu_traffic_test
+"
+
+NUM_NETIFS=2
+source lib.sh
+
+h1_create()
+{
+	simple_if_init $h1
+	vlan_create $h1 10 v$h1 192.0.2.2/24 2001:db8:1::2/64
+}
+
+h1_destroy()
+{
+	vlan_destroy $h1 10 192.0.2.2/24 2001:db8:1::2/64
+	simple_if_fini $h1
+}
+
+switch_create()
+{
+	ip li set dev $swp1 up
+	vlan_create $swp1 10 "" 192.0.2.1/24 2001:db8:1::1/64
+}
+
+switch_destroy()
+{
+	ip li set dev $swp1 down
+	vlan_destroy $swp1 10
+}
+
+setup_prepare()
+{
+	h1=${NETIFS[p1]}
+	swp1=${NETIFS[p2]}
+
+	vrf_prepare
+
+	h1_create
+
+	switch_create
+
+	forwarding_enable
+}
+
+cleanup()
+{
+	pre_cleanup
+
+	forwarding_restore
+
+	switch_destroy
+
+	h1_destroy
+
+	vrf_cleanup
+}
+
+ping_ipv4()
+{
+	ping_test $h1.10 192.0.2.1
+}
+
+ping_ipv6()
+{
+	ping6_test $h1.10 2001:db8:1::1
+}
+
+min_max_mtu_get_if()
+{
+	local dev=$1; shift
+	local min_max=$1; shift
+
+	ip -d -j link show $dev | jq ".[].$min_max"
+}
+
+ensure_compatible_min_max_mtu()
+{
+	local min_max=$1; shift
+
+	local mtu=$(min_max_mtu_get_if ${NETIFS[p1]} $min_max)
+	local i
+
+	for ((i = 2; i <= NUM_NETIFS; ++i)); do
+		local current_mtu=$(min_max_mtu_get_if ${NETIFS[p$i]} $min_max)
+
+		if [ $current_mtu -ne $mtu ]; then
+			return 1
+		fi
+	done
+}
+
+mtu_set_if()
+{
+	local dev=$1; shift
+	local mtu=$1; shift
+	local should_fail=${1:-0}; shift
+
+	mtu_set $dev $mtu 2>/dev/null
+	check_err_fail $should_fail $? "Set MTU $mtu for $dev"
+}
+
+mtu_set_all_if()
+{
+	local mtu=$1; shift
+	local i
+
+	for ((i = 1; i <= NUM_NETIFS; ++i)); do
+		mtu_set_if ${NETIFS[p$i]} $mtu
+		mtu_set_if ${NETIFS[p$i]}.10 $mtu
+	done
+}
+
+mtu_restore_all_if()
+{
+	local i
+
+	for ((i = 1; i <= NUM_NETIFS; ++i)); do
+		mtu_restore ${NETIFS[p$i]}.10
+		mtu_restore ${NETIFS[p$i]}
+	done
+}
+
+mtu_test_ping4()
+{
+	local mtu=$1; shift
+	local should_fail=$1; shift
+
+	# Ping adds 8 bytes for ICMP header and 20 bytes for IP header
+	local ping_headers_len=$((20 + 8))
+	local pkt_size=$((mtu - ping_headers_len))
+
+	ping_do $h1.10 192.0.2.1 "-s $pkt_size -M do"
+	check_err_fail $should_fail $? "Ping, packet size: $pkt_size"
+}
+
+mtu_test_ping6()
+{
+	local mtu=$1; shift
+	local should_fail=$1; shift
+
+	# Ping adds 8 bytes for ICMP header and 40 bytes for IPv6 header
+	local ping6_headers_len=$((40 + 8))
+	local pkt_size=$((mtu - ping6_headers_len))
+
+	ping6_do $h1.10 2001:db8:1::1 "-s $pkt_size -M do"
+	check_err_fail $should_fail $? "Ping6, packet size: $pkt_size"
+}
+
+max_mtu_config_test()
+{
+	local i
+
+	RET=0
+
+	for ((i = 1; i <= NUM_NETIFS; ++i)); do
+		local dev=${NETIFS[p$i]}
+		local max_mtu=$(min_max_mtu_get_if $dev "max_mtu")
+		local should_fail
+
+		should_fail=0
+		mtu_set_if $dev $max_mtu $should_fail
+		mtu_restore $dev
+
+		should_fail=1
+		mtu_set_if $dev $((max_mtu + 1)) $should_fail
+		mtu_restore $dev
+	done
+
+	log_test "Test maximum MTU configuration"
+}
+
+max_mtu_traffic_test()
+{
+	local should_fail
+	local max_mtu
+
+	RET=0
+
+	if ! ensure_compatible_min_max_mtu "max_mtu"; then
+		log_test_xfail "Topology has incompatible maximum MTU values"
+		return
+	fi
+
+	max_mtu=$(min_max_mtu_get_if ${NETIFS[p1]} "max_mtu")
+
+	should_fail=0
+	mtu_set_all_if $max_mtu
+	mtu_test_ping4 $max_mtu $should_fail
+	mtu_test_ping6 $max_mtu $should_fail
+	mtu_restore_all_if
+
+	should_fail=1
+	mtu_set_all_if $((max_mtu - 1))
+	mtu_test_ping4 $max_mtu $should_fail
+	mtu_test_ping6 $max_mtu $should_fail
+	mtu_restore_all_if
+
+	log_test "Test traffic, packet size is maximum MTU"
+}
+
+min_mtu_config_test()
+{
+	local i
+
+	RET=0
+
+	for ((i = 1; i <= NUM_NETIFS; ++i)); do
+		local dev=${NETIFS[p$i]}
+		local min_mtu=$(min_max_mtu_get_if $dev "min_mtu")
+		local should_fail
+
+		should_fail=0
+		mtu_set_if $dev $min_mtu $should_fail
+		mtu_restore $dev
+
+		should_fail=1
+		mtu_set_if $dev $((min_mtu - 1)) $should_fail
+		mtu_restore $dev
+	done
+
+	log_test "Test minimum MTU configuration"
+}
+
+min_mtu_traffic_test()
+{
+	local should_fail=0
+	local min_mtu
+
+	RET=0
+
+	if ! ensure_compatible_min_max_mtu "min_mtu"; then
+		log_test_xfail "Topology has incompatible minimum MTU values"
+		return
+	fi
+
+	min_mtu=$(min_max_mtu_get_if ${NETIFS[p1]} "min_mtu")
+	mtu_set_all_if $min_mtu
+	mtu_test_ping4 $min_mtu $should_fail
+	# Do not test minimum MTU with IPv6, as IPv6 requires higher MTU.
+
+	mtu_restore_all_if
+
+	log_test "Test traffic, packet size is minimum MTU"
+}
+
+trap cleanup EXIT
+
+setup_prepare
+setup_wait
+
+tests_run
+
+exit $EXIT_STATUS