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

Ujjal Roy
2026-04-03T15:01:37Z

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 <royujjal@gmail.com>
---
.../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
+mc_decode
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=""
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
+}
+
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"

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:"
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
+ 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"
+
+ echo "Vlan 10 mcast_query_response_interval (MRC) test cases:"
+ 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