Subject: Re: [PATCH net-next v3 5/5] selftests: net: bridge: add tests for MRC and QQIC validation

From: Ido Schimmel <[email-masked]>
Date: Tue, 7 Apr 2026 16:45:28 +0300

On Fri, Apr 03, 2026 at 03:00:50PM +0000, Ujjal Roy wrote:
> Update bridge selftests that configure Max Resp Time (MRT) and Querier
> Query Interval (QQI) parameters and validate the resulting Query packet
> fields for Max Resp Code (MRC) and Querier Query Interval Code (QQIC).
> 
> These tests cover the IGMPv3 and MLDv2 for below cases:
> * MRC and QQIC in linear range.
> * MRC and QQIC in non-linear range.
> 
> TEST: Vlan multicast snooping enable                                [ OK ]
> TEST: Vlan mcast_query_interval global option default value         [ OK ]
> Vlan 10 mcast_query_interval (QQIC) test cases:
> TEST: Number of tagged IGMPv2 general query                         [ OK ]
> TEST: IGMPv3 QQIC linear value 60                                   [ OK ]
> TEST: MLDv2 QQIC linear value 60                                    [ OK ]
> TEST: IGMPv3 QQIC non linear value 160                              [ OK ]
> TEST: MLDv2 QQIC non linear value 160                               [ OK ]
> TEST: Vlan mcast_query_response_interval global option default value   [ OK ]
> Vlan 10 mcast_query_response_interval (MRC) test cases:
> TEST: IGMPv3 MRC linear value 60                                    [ OK ]
> TEST: IGMPv3 MRC non linear value 160                               [ OK ]
> TEST: MLDv2 MRC linear value 30000                                  [ OK ]
> TEST: MLDv2 MRC non linear value 60000                              [ OK ]
> 
> Signed-off-by: Ujjal Roy <[email-masked]>
> ---
>  .../selftests/net/forwarding/.gitignore       |   2 +
>  .../testing/selftests/net/forwarding/Makefile |  10 ++
>  .../net/forwarding/bridge_vlan_mcast.sh       | 157 +++++++++++++++++-
>  .../selftests/net/forwarding/mc_decode.c      |  73 ++++++++
>  .../selftests/net/forwarding/mc_encode.c      |  78 +++++++++
>  5 files changed, 315 insertions(+), 5 deletions(-)
>  create mode 100644 tools/testing/selftests/net/forwarding/mc_decode.c
>  create mode 100644 tools/testing/selftests/net/forwarding/mc_encode.c
> 
> diff --git a/tools/testing/selftests/net/forwarding/.gitignore b/tools/testing/selftests/net/forwarding/.gitignore
> index 418ff96c52ef..aa0c7f1afb4b 100644
> --- a/tools/testing/selftests/net/forwarding/.gitignore
> +++ b/tools/testing/selftests/net/forwarding/.gitignore
> @@ -1,3 +1,5 @@
>  # SPDX-License-Identifier: GPL-2.0-only
>  forwarding.config
>  ipmr
> +mc_encode

Do we really need this binary and all the added complexity? You can just
hard code in the test the expected encoded form of the non-linear values

> +mc_decode

This one doesn't seem to be used

> diff --git a/tools/testing/selftests/net/forwarding/Makefile b/tools/testing/selftests/net/forwarding/Makefile
> index bbaf4d937dd8..a26da846632d 100644
> --- a/tools/testing/selftests/net/forwarding/Makefile
> +++ b/tools/testing/selftests/net/forwarding/Makefile
> @@ -1,5 +1,15 @@
>  # SPDX-License-Identifier: GPL-2.0+ OR MIT
>  
> +top_srcdir = ../../../../..
> +
> +CFLAGS += -Wall -Wl,--no-as-needed -O2 -g -I$(top_srcdir)/usr/include $(KHDR_INCLUDES)
> +CFLAGS += -I$(top_srcdir)/tools/include
> +
> +TEST_GEN_FILES := \
> +	mc_encode \
> +	mc_decode \
> +# end of TEST_GEN_FILES
> +
>  TEST_PROGS := \
>  	bridge_activity_notify.sh \
>  	bridge_fdb_learning_limit.sh \
> diff --git a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
> index 72dfbeaf56b9..89598712f869 100755
> --- a/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
> +++ b/tools/testing/selftests/net/forwarding/bridge_vlan_mcast.sh
> @@ -5,6 +5,7 @@ ALL_TESTS="vlmc_control_test vlmc_querier_test vlmc_igmp_mld_version_test \
>  	   vlmc_last_member_test vlmc_startup_query_test vlmc_membership_test \
>  	   vlmc_querier_intvl_test vlmc_query_intvl_test vlmc_query_response_intvl_test \
>  	   vlmc_router_port_test vlmc_filtering_test"
> +TEST_NAME=""

Why is this needed? Just pass to vlmc_query_get_intvl_match() an
argument that indicates if we need MRC / QQIC

>  NUM_NETIFS=4
>  CHECK_TC="yes"
>  TEST_GROUP="239.10.10.10"
> @@ -96,6 +97,17 @@ cleanup()
>  	vrf_cleanup
>  }
>  
> +check_binary()
> +{
> +	local cmd=$1; shift
> +	local args=$@
> +
> +	if [[ ! -x "$(command -v "$cmd")" ]]; then
> +		log_test_skip "$args $cmd not found"
> +		return $EXIT_STATUS
> +	fi
> +}

This can be removed as well

> +
>  vlmc_v2join_test()
>  {
>  	local expect=$1
> @@ -162,14 +174,27 @@ vlmc_query_cnt_setup()
>  {
>  	local type=$1
>  	local dev=$2
> +	local intvl_match="$3"
>  
>  	if [[ $type == "igmp" ]]; then
> +		# This matches: IP Protocol 2 (IGMP)
>  		tc filter add dev $dev egress pref 10 prot 802.1Q \
>  			flower vlan_id 10 vlan_ethtype ipv4 dst_ip 224.0.0.1 ip_proto 2 \
> +			action continue
> +		# AND Type 0x11 (Query) at offset 24 after IP
> +		# IP (20 byte IP + 4 bytes Option)
> +		tc filter add dev $dev egress pref 20 prot 802.1Q u32 \
> +			match u8 0x11 0xff at 24 $intvl_match \
>  			action pass
>  	else
> +		# This matches: ICMPv6
>  		tc filter add dev $dev egress pref 10 prot 802.1Q \
>  			flower vlan_id 10 vlan_ethtype ipv6 dst_ip ff02::1 ip_proto icmpv6 \
> +			action continue
> +		# AND Type 0x82 (Query) at offset 48 after IPv6
> +		# IPv6 (40 bytes IPv6 + 2 bytes next HDR + 4 bytes Option + 2 byte pad)
> +		tc filter add dev $dev egress pref 20 prot 802.1Q u32 \
> +			match u8 0x82 0xff at 48 $intvl_match \
>  			action pass
>  	fi
>  
> @@ -181,9 +206,46 @@ vlmc_query_cnt_cleanup()
>  	local dev=$1
>  
>  	ip link set dev br0 type bridge mcast_stats_enabled 0
> +	tc filter del dev $dev egress pref 20
>  	tc filter del dev $dev egress pref 10
>  }
>  
> +vlmc_query_get_intvl_match()
> +{
> +	local type=$1
> +	local version=$2
> +	local interval=$3
> +	local encode=""
> +
> +	if [ "$interval" = "" ]; then
> +		return
> +	fi
> +
> +	if [ "$TEST_NAME" = "vlmc_query_intvl_test" ]; then
> +		# QQIC is 8-bit floating point encoding for IGMPv3 and MLDv2
> +		encode="$(./mc_encode 8 $interval)"
> +		if [ "${type}v${version}" = "igmpv3" ]; then
> +			# IP 20 bytes + 4 bytes Option + IGMPv3[9]
> +			echo "match u8 $encode 0xff at 33"
> +		elif [ "${type}v${version}" = "mldv2" ]; then
> +			# IPv6 40 + 2 next HDR + 4 Option + 2 pad + MLDv2[25]
> +			echo "match u8 $encode 0xff at 73"
> +		fi
> +	elif [ "$TEST_NAME" = "vlmc_query_response_intvl_test" ]; then
> +		if [ "${type}v${version}" = "igmpv3" ]; then
> +			# MRC is 8-bit floating point encoding for MLDv2
> +			encode="$(./mc_encode 8 $interval)"
> +			# IP 20 bytes + 4 bytes Option + IGMPv3[1]
> +			echo "match u8 $encode 0xff at 25"
> +		elif [ "${type}v${version}" = "mldv2" ]; then
> +			# MRC is 16-bit floating point encoding for MLDv2
> +			encode="$(./mc_encode 16 $interval)"
> +			# IPv6 40 + 2 next HDR + 4 Option + 2 pad + MLDv2[4]
> +			echo "match u16 $encode 0xffff at 52"
> +		fi
> +	fi
> +}
> +
>  vlmc_check_query()
>  {
>  	local type=$1
> @@ -191,9 +253,12 @@ vlmc_check_query()
>  	local dev=$3
>  	local expect=$4
>  	local time=$5
> +	local interval=$6
> +	local intvl_match=""
>  	local ret=0
>  
> -	vlmc_query_cnt_setup $type $dev
> +	intvl_match="$(vlmc_query_get_intvl_match $type $version $interval)"
> +	vlmc_query_cnt_setup $type $dev "$intvl_match"

Did you run shellcheck? I believe it will issue a warning if you don't
quote "$type" and "$dev". I realize it differs from other places in the
test, but we try to avoid adding new warnings / errors.

>  
>  	local pre_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev)
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_querier 1
> @@ -201,7 +266,7 @@ vlmc_check_query()
>  	if [[ $ret -eq 0 ]]; then
>  		sleep $time
>  
> -		local tcstats=$(tc_rule_stats_get $dev 10 egress)
> +		local tcstats=$(tc_rule_stats_get $dev 20 egress)
>  		local post_tx_xstats=$(vlmc_query_cnt_xstats $type $version $dev)
>  
>  		if [[ $tcstats != $expect || \
> @@ -428,6 +493,10 @@ vlmc_querier_intvl_test()
>  
>  vlmc_query_intvl_test()
>  {
> +	TEST_NAME="vlmc_query_intvl_test"
> +
> +	check_binary "./mc_encode" "$TEST_NAME: verify" || return 1
> +
>  	RET=0
>  	local goutput=`bridge -j vlan global show`
>  	echo -n $goutput |
> @@ -440,6 +509,7 @@ vlmc_query_intvl_test()
>  	check_err $? "Wrong default mcast_query_interval global vlan option value"
>  	log_test "Vlan mcast_query_interval global option default value"
>  
> +	echo "Vlan 10 mcast_query_interval (QQIC) test cases:"

log_info()

>  	RET=0
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 0
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 200
> @@ -447,14 +517,52 @@ vlmc_query_intvl_test()
>  	# 1 is sent immediately, then 2 more in the next 5 seconds
>  	vlmc_check_query igmp 2 $swp1 3 5
>  	check_err $? "Wrong number of tagged IGMPv2 general queries sent"
> -	log_test "Vlan 10 mcast_query_interval option changed to 200"
> +	log_test "Number of tagged IGMPv2 general query"
> +
> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3
> +	check_err $? "Could not set mcast_igmp_version in vlan 10"
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2
> +	check_err $? "Could not set mcast_mld_version in vlan 10"
> +
> +	RET=0

What's the point in the check_err() above if there is no log_test()
before resetting RET? Let's remove them

> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 6000
> +	check_err $? "Could not set mcast_query_interval in vlan 10"
> +	# 1 is sent immediately, IGMPv3 QQIC should match with linear value 60s
> +	vlmc_check_query igmp 3 $swp1 1 1 60
> +	check_err $? "Wrong QQIC in sent tagged IGMPv3 general queries"
> +	log_test "IGMPv3 QQIC linear value 60"
> +	RET=0
> +	# 1 is sent immediately, MLDv2 QQIC should match with linear value 60s
> +	vlmc_check_query mld 2 $swp1 1 1 60
> +	check_err $? "Wrong QQIC in sent tagged MLDv2 general queries"
> +	log_test "MLDv2 QQIC linear value 60"
>  
> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 16000
> +	check_err $? "Could not set mcast_query_interval in vlan 10"
> +	# 1 is sent immediately, IGMPv3 QQIC should match with non linear value 160s
> +	vlmc_check_query igmp 3 $swp1 1 1 160
> +	check_err $? "Wrong QQIC in sent tagged IGMPv3 general queries"
> +	log_test "IGMPv3 QQIC non linear value 160"
> +	RET=0
> +	# 1 is sent immediately, MLDv2 QQIC should match with non linear value 160s
> +	vlmc_check_query mld 2 $swp1 1 1 160
> +	check_err $? "Wrong QQIC in sent tagged MLDv2 general queries"
> +	log_test "MLDv2 QQIC non linear value 160"
> +
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_interval 12500
>  }
>  
>  vlmc_query_response_intvl_test()
>  {
> +	TEST_NAME="vlmc_query_response_intvl_test"
> +
> +	check_binary "./mc_encode" "$TEST_NAME: verify" || return 1
> +
>  	RET=0
>  	local goutput=`bridge -j vlan global show`
>  	echo -n $goutput |
> @@ -468,10 +576,49 @@ vlmc_query_response_intvl_test()
>  	log_test "Vlan mcast_query_response_interval global option default value"
>  
>  	RET=0
> -	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 200
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 3
> +	check_err $? "Could not set mcast_igmp_version in vlan 10"

Can be removed

> +
> +	echo "Vlan 10 mcast_query_response_interval (MRC) test cases:"

log_info()

> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 600
> +	check_err $? "Could not set mcast_query_response_interval in vlan 10"
> +	# 1 is sent immediately, IGMPv3 MRC should match with linear value 60 units of 1/10s
> +	vlmc_check_query igmp 3 $swp1 1 1 60
> +	check_err $? "Wrong MRC in sent tagged IGMPv3 general queries"
> +	log_test "IGMPv3 MRC linear value 60"
> +
> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 1600
> +	check_err $? "Could not set mcast_query_response_interval in vlan 10"
> +	# 1 is sent immediately, IGMPv3 MRC should match with non linear value 160 unit of 1/10s
> +	vlmc_check_query igmp 3 $swp1 1 1 160
> +	check_err $? "Wrong MRC in sent tagged IGMPv3 general queries"
> +	log_test "IGMPv3 MRC non linear value 160"
> +
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 2
> +	check_err $? "Could not set mcast_mld_version in vlan 10"
> +
> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 3000
> +	check_err $? "Could not set mcast_query_response_interval in vlan 10"
> +	# 1 is sent immediately, MLDv2 MRC should match with linear value 30000(ms)
> +	vlmc_check_query mld 2 $swp1 1 1 30000
> +	check_err $? "Wrong MRC in sent tagged MLDv2 general queries"
> +	log_test "MLDv2 MRC linear value 30000"
> +
> +	RET=0
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 6000
>  	check_err $? "Could not set mcast_query_response_interval in vlan 10"
> -	log_test "Vlan 10 mcast_query_response_interval option changed to 200"
> +	# 1 is sent immediately, MLDv2 MRC should match with non linear value 60000(ms)
> +	vlmc_check_query mld 2 $swp1 1 1 60000
> +	check_err $? "Wrong MRC in sent tagged MLDv2 general queries"
> +	log_test "MLDv2 MRC non linear value 60000"
>  
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_igmp_version 2
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_mld_version 1
> +	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_startup_query_count 2
>  	bridge vlan global set vid 10 dev br0 mcast_snooping 1 mcast_query_response_interval 1000
>  }
>  
> diff --git a/tools/testing/selftests/net/forwarding/mc_decode.c b/tools/testing/selftests/net/forwarding/mc_decode.c
> new file mode 100644
> index 000000000000..2c5e784226e4
> --- /dev/null
> +++ b/tools/testing/selftests/net/forwarding/mc_decode.c
> @@ -0,0 +1,73 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <linux/bitops.h>
> +
> +/* 8-bit floating-point exponential field decode */
> +#define FP_8BIT_EXP(value)		(((value) >> 4) & 0x07)
> +#define FP_8BIT_MAN(value)		((value) & 0x0f)
> +
> +/* 16-bit floating-point exponential field decode */
> +#define FP_16BIT_EXP(value)		(((value) >> 12) & 0x0007)
> +#define FP_16BIT_MAN(value)		((value) & 0x0fff)
> +
> +/* 8-bit floating-point exponential field linear threshold */
> +#define FP_8BIT_MIN_THRESHOLD		128
> +/* 8-bit non linear max representable (mant = 0xF, exp = 7) -> 31744 */
> +#define FP_8BIT_MAX_THRESHOLD		31744
> +
> +/* 16-bit floating-point exponential field linear threshold */
> +#define FP_16BIT_MIN_THRESHOLD		32768UL
> +/* 16-bit non linear max representable (mant = 0xFFF, exp = 7) -> 8387584 */
> +#define FP_16BIT_MAX_THRESHOLD		8387584
> +
> +/* This decodes 8-bit floating-point exponential values */
> +static inline uint32_t decode_8bit_field(const u8 code)
> +{
> +	if (code < FP_8BIT_MIN_THRESHOLD) {
> +		return code;
> +	} else {
> +		uint32_t mc_man, mc_exp;
> +
> +		mc_exp = FP_8BIT_EXP(code);
> +		mc_man = FP_8BIT_MAN(code);
> +		return (mc_man | 0x10) << (mc_exp + 3);
> +	}
> +}
> +
> +/* This decodes 16-bit floating-point exponential values */
> +static inline uint32_t decode_16bit_field(const uint16_t code)
> +{
> +	if (code < FP_16BIT_MIN_THRESHOLD) {
> +		return code;
> +	} else {
> +		uint32_t mc_man, mc_exp;
> +
> +		mc_exp = FP_16BIT_EXP(code);
> +		mc_man = FP_16BIT_MAN(code);
> +
> +		return (mc_man | 0x1000) << (mc_exp + 3);
> +	}
> +}
> +
> +int main(int argc, char *argv[])
> +{
> +	uint32_t bits = 8, code = 0, decode = 0;
> +
> +	if (argc != 3)
> +		return 1;
> +
> +	if (bits != 8 && bits != 16)
> +		return 1;
> +
> +	bits = atoi(argv[1]);
> +	code = atoi(argv[2]);
> +
> +	if (bits == 8)
> +		decode = decode_8bit_field(code);
> +	else
> +		decode = decode_16bit_field(code);
> +	printf("%u\n", decode);
> +
> +	return 0;
> +}
> diff --git a/tools/testing/selftests/net/forwarding/mc_encode.c b/tools/testing/selftests/net/forwarding/mc_encode.c
> new file mode 100644
> index 000000000000..24d9bd9299cc
> --- /dev/null
> +++ b/tools/testing/selftests/net/forwarding/mc_encode.c
> @@ -0,0 +1,78 @@
> +// SPDX-License-Identifier: GPL-2.0
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <linux/bitops.h>
> +
> +/* 8-bit floating-point exponential field linear threshold */
> +#define FP_8BIT_MIN_THRESHOLD		128
> +/* 8-bit non linear max representable (mant = 0xF, exp = 7) -> 31744 */
> +#define FP_8BIT_MAX_THRESHOLD		31744
> +
> +/* 16-bit floating-point exponential field linear threshold */
> +#define FP_16BIT_MIN_THRESHOLD		32768UL
> +/* 16-bit non linear max representable (mant = 0xFFF, exp = 7) -> 8387584 */
> +#define FP_16BIT_MAX_THRESHOLD		8387584
> +
> +/* This encodes value to 8-bit floating-point exponential format */
> +static inline uint8_t encode_8bit_field(unsigned int value)
> +{
> +	uint8_t mc_exp, mc_man;
> +
> +	/* Value < 128 is literal */
> +	if (value < FP_8BIT_MIN_THRESHOLD)
> +		return value;
> +
> +	/* Saturate at max representable (mant = 0xF, exp = 7) -> 31744 */
> +	if (value >= FP_8BIT_MAX_THRESHOLD)
> +		return 0xFF;
> +
> +	mc_exp  = fls(value) - 8;
> +	mc_man = (value >> (mc_exp + 3)) & 0x0F;
> +
> +	return 0x80 | (mc_exp << 4) | mc_man;
> +}
> +
> +/* This encodes value to 16-bit floating-point exponential format */
> +static inline uint16_t encode_16bit_field(unsigned int value)
> +{
> +	uint16_t mc_man, mc_exp;
> +
> +	/* Value < 32768 is literal */
> +	if (value < FP_16BIT_MIN_THRESHOLD)
> +		return value;
> +
> +	/* Saturate at max representable (mant = 0xFFF, exp = 7) -> 8387584 */
> +	if (value >= FP_16BIT_MAX_THRESHOLD)
> +		return 0xFFFF;
> +
> +	mc_exp = fls(value) - 16;
> +	mc_man = (value >> (mc_exp + 3)) & 0x0FFF;
> +
> +	return 0x8000 | (mc_exp << 12) | mc_man;
> +}
> +
> +int main(int argc, char *argv[])
> +{
> +	unsigned int bits = 8, value = 0;
> +	uint8_t encoded8 = 0;
> +	uint16_t encoded16 = 0;
> +
> +	if (argc != 3)
> +		return 1;
> +
> +	bits = atoi(argv[1]);
> +	value = atoi(argv[2]);
> +
> +	if (bits != 8 && bits != 16)
> +		return 1;
> +
> +	if (bits == 8) {
> +		encoded8 = encode_8bit_field(value);
> +		printf("%hhu\n", encoded8);
> +	} else {
> +		encoded16 = encode_16bit_field(value);
> +		printf("%hu\n", encoded16);
> +	}
> +
> +	return 0;
> +}
> -- 
> 2.43.0
>