Netdev List
 help / color / mirror / Atom feed
From: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
To: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
	linux-kselftest@vger.kernel.org
Cc: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
	pabeni@redhat.com, jhs@mojatatu.com, jiri@resnulli.us,
	horms@kernel.org, shuah@kernel.org, vladimir.oltean@nxp.com,
	vinicius.gomes@intel.com, fejes@inf.elte.hu,
	xiaoliang.yang_1@nxp.com
Subject: [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action
Date: Mon, 22 Jun 2026 17:21:18 +0800	[thread overview]
Message-ID: <20260622092118.6846-7-xiaoliang.yang_1@nxp.com> (raw)
In-Reply-To: <20260622092118.6846-1-xiaoliang.yang_1@nxp.com>

Add frer_test.sh, a TAP-format kselftest script covering the FRER
(IEEE 802.1CB Frame Replication and Elimination for Reliability)
tc action (act_frer).

Tests 1-4 use a bond-based two-namespace topology:

  ns_talker
  +---------------------------+
  | bond0 (IP_SRC, balance-rr)|
  |   slave: veth_a0 (frer push + mirror to veth_b0)|
  |   slave: veth_b0 (frer push + mirror to veth_a0)|
  +-------+---------------+--+
          |               |
     veth_a0         veth_b0
          |               |
     veth_a1         veth_b1
          |               |
  +-------+---------------+--+
  | bond1 (IP_DST, balance-rr)|
  |   slave: veth_a1 (frer recover ingress)          |
  |   slave: veth_b1 (frer recover ingress)          |
  +---------------------------+
  ns_listener

  IP_SRC is assigned to bond0; IP_DST is assigned to bond1.  FRER push
  is configured on both veth_a0 and veth_b0 egress with cross-mirroring
  so every frame sent by either bond slave carries an R-TAG and a
  mirrored copy reaches the peer slave.  Tests 1-4 exercise shared and
  individual recover modes on the listener side.

Test 5 uses a self-contained single-path (no bond) topology:

  ns_p2p_src                        ns_p2p_dst
  +----------------------+          +----------------------+
  | frer_p2p_a0 (IP_P2P_SRC)| <---> | frer_p2p_a1 (IP_P2P_DST)|
  | egress: frer push     |          | ingress: frer recover |
  +----------------------+          +----------------------+

Test 6 uses a four-namespace relay topology:

  ns_talker -- bridge0 (br_r0) -+- path A -+- bridge1 (br_r1) -- ns_listener
                                 \- path B -/

  bridge0 acts as sequence generator (frer push + replicate to both
  redundant paths); bridge1 acts as eliminator (frer shared recover with
  tag-pop on both ingress ports).

Six functional test cases are included:

  1. push verify              - confirm that the frer push action inserts
                                an R-TAG (EtherType 0xF1C1) on egress;
                                tcpdump on both veth_a1 and veth_b1 must
                                capture at least one R-TAG frame each.

  2. shared recover e2e       - veth_a1 and veth_b1 share one recover
                                action; the action passes exactly one copy
                                and discards the duplicate; verified via
                                ping success, tcpdump frame count on bond1,
                                and tc stats (passed >= PING_COUNT,
                                discarded >= PING_COUNT).

  3. individual recover       - veth_a1 and veth_b1 use independent recover
                                actions so both copies are passed without
                                cross-port deduplication; verified via
                                per-slave tcpdump and tc stats
                                (discarded = 0 on each port).

  4. no tag-pop               - shared recover without tag-pop leaves the
                                R-TAG on passed frames; verified by
                                capturing EtherType 0xF1C1 (expect >= 1)
                                and plain ICMP (expect 0) on bond1.

  5. simple point-to-point    - single-path push + individual recover (with
                                tag-pop) end-to-end ping test; no bond.

  6. relay e2e                - four-namespace bridge relay topology; bridge0
                                pushes R-TAG and replicates to two paths;
                                bridge1 recovers (shared, tag-pop) and
                                forwards deduplicated frames to listener;
                                verified via ping success, tcpdump frame
                                count on listener, and bridge1 tc stats.

The script conforms to the kselftest framework (TAP output, KSFT_PASS /
KSFT_FAIL / KSFT_SKIP exit codes).  It loads kselftest/lib.sh when
available and falls back to a minimal inline implementation otherwise.
All tests are skipped gracefully when act_frer is not available in the
running kernel.

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 tools/testing/selftests/net/Makefile     |    1 +
 tools/testing/selftests/net/frer_test.sh | 1013 ++++++++++++++++++++++
 2 files changed, 1014 insertions(+)
 create mode 100755 tools/testing/selftests/net/frer_test.sh

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6a190a525a39..67b896611f08 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -38,6 +38,7 @@ TEST_PROGS := \
 	fib_rule_tests.sh \
 	fib_tests.sh \
 	fin_ack_lat.sh \
+	frer_test.sh \
 	fq_band_pktlimit.sh \
 	gre_gso.sh \
 	gre_ipv6_lladdr.sh \
diff --git a/tools/testing/selftests/net/frer_test.sh b/tools/testing/selftests/net/frer_test.sh
new file mode 100755
index 000000000000..ecd88952f495
--- /dev/null
+++ b/tools/testing/selftests/net/frer_test.sh
@@ -0,0 +1,1013 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Copyright 2026 NXP
+#
+# frer_test.sh - IEEE 802.1CB FRER tc action kselftest
+#
+# Topology for tests 1-4:
+#
+#   ns_talker  bond0 (veth_a0 + veth_b0)  <--->  bond1 (veth_a1 + veth_b1)  ns_listener
+#
+#   IP_SRC assigned to bond0;  IP_DST assigned to bond1
+#
+#   bond mode: balance-rr (round-robin), so frames are distributed across
+#              both slaves.  FRER push is configured on both veth_a0 and
+#              veth_b0 egress with cross-mirror so every frame sent by either
+#              slave carries an R-TAG and a mirrored copy reaches the peer.
+#   FRER recover: veth_a1/veth_b1 ingress, shared or individual recover per test
+#
+#   Ping runs from bond0 to bond1; tcpdump captures on bond1 (or on individual
+#   slave interfaces for tests where both copies must be observable).
+#
+# Test 5: simple point-to-point, self-contained topology (no bond).
+# Test 6: relay system, self-contained topology.
+#
+# All namespaces, veth pairs, bond interfaces, tc rules and addresses are
+# created and destroyed within this script.  External dependencies:
+#   - kernel with CONFIG_NET_ACT_FRER and CONFIG_BONDING
+#   - iproute2 tc with frer action support
+#   - tcpdump, ping
+#   - root privileges
+
+# ----------------------------------------------------------------------------
+# kselftest library: TAP output + exit-code constants
+# ----------------------------------------------------------------------------
+ksft_lib="${KSFT_LIB:-$(dirname "$0")/../kselftest/lib.sh}"
+if [ -f "$ksft_lib" ]; then
+	# shellcheck source=/dev/null
+	. "$ksft_lib"
+else
+	# Minimal fallback when run outside the kselftest tree
+	KSFT_PASS=0
+	KSFT_FAIL=1
+	KSFT_SKIP=4
+	_ksft_count=0
+	_ksft_pass=0
+	_ksft_fail=0
+	_ksft_skip=0
+
+	ksft_print_header() { echo "TAP version 13"; }
+	ksft_set_plan()     { echo "1..$1"; }
+	ksft_test_result_pass() {
+		_ksft_count=$((_ksft_count + 1)); _ksft_pass=$((_ksft_pass + 1))
+		echo "ok $_ksft_count - $*"
+	}
+	ksft_test_result_fail() {
+		_ksft_count=$((_ksft_count + 1)); _ksft_fail=$((_ksft_fail + 1))
+		echo "not ok $_ksft_count - $*"
+	}
+	ksft_test_result_skip() {
+		_ksft_count=$((_ksft_count + 1)); _ksft_skip=$((_ksft_skip + 1))
+		echo "ok $_ksft_count - $* # SKIP"
+	}
+	ksft_print_cnts() {
+		echo "# Totals: pass=$_ksft_pass fail=$_ksft_fail skip=$_ksft_skip"
+	}
+	ksft_exit_pass()     { exit $KSFT_PASS; }
+	ksft_exit_fail()     { exit $KSFT_FAIL; }
+	ksft_exit_fail_msg() { echo "# FATAL: $*" >&2; exit $KSFT_FAIL; }
+fi
+
+# ----------------------------------------------------------------------------
+# Configuration (override via environment)
+# ----------------------------------------------------------------------------
+TC="${TC:-tc}"
+PING="${PING:-ping}"
+TCPDUMP="${TCPDUMP:-tcpdump}"
+PING_COUNT="${PING_COUNT:-5}"
+PING_TIMEOUT="${PING_TIMEOUT:-2}"
+SKIP_MODPROBE="${SKIP_MODPROBE:-0}"
+
+# Bond topology interfaces (tests 1-4)
+readonly VETH_A0="frer_a0"
+readonly VETH_A1="frer_a1"
+readonly VETH_B0="frer_b0"
+readonly VETH_B1="frer_b1"
+readonly BOND0="frer_bond0"
+readonly BOND1="frer_bond1"
+
+readonly NS_TALKER="frer_ns_talker"
+readonly NS_LISTENER="frer_ns_listener"
+
+readonly IP_SRC="10.0.0.1"
+readonly IP_DST="10.0.0.2"
+
+# Point-to-point topology interfaces (test 5)
+readonly P2P_NS_SRC="frer_p2p_src"
+readonly P2P_NS_DST="frer_p2p_dst"
+readonly P2P_VETH_A0="frer_p2p_a0"
+readonly P2P_VETH_A1="frer_p2p_a1"
+readonly IP_P2P_SRC="10.0.1.1"
+readonly IP_P2P_DST="10.0.1.2"
+
+# Relay topology interfaces (test 6)
+#
+#   ns_talker (talker_eth.100) -- talker_eth/br0_uplink -- bridge0 (br_r0)
+#                                         |-- br0_swp0/br1_swp0 --\
+#                                         \-- br0_swp1/br1_swp1 --+--\
+#		bridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener
+#
+# bridge0 acts as sequence generator (frer push + replicate to both paths).
+# bridge1 acts as eliminator (frer recover, shared, tag-pop).
+readonly R_NS_TALKER="frer_r_talker"
+readonly R_NS_BRIDGE0="frer_r_bridge0"
+readonly R_NS_BRIDGE1="frer_r_bridge1"
+readonly R_NS_LISTENER="frer_r_listener"
+readonly R_TALKER_ETH="r_tlk_eth"       # talker-side physical port
+readonly R_BR0_UPLINK="r_br0_uplink"    # bridge0 uplink facing talker
+readonly R_BR0_SWP0="r_br0_swp0"        # bridge0 redundant path port 0
+readonly R_BR0_SWP1="r_br0_swp1"        # bridge0 redundant path port 1
+readonly R_BR1_SWP0="r_br1_swp0"        # bridge1 redundant path port 0
+readonly R_BR1_SWP1="r_br1_swp1"        # bridge1 redundant path port 1
+readonly R_BR1_DOWNLINK="r_br1_dwnlnk"  # bridge1 downlink facing listener
+readonly R_LISTENER_ETH="r_lst_eth"     # listener-side physical port
+readonly R_BR0="br_r0"
+readonly R_BR1="br_r1"
+readonly R_VLAN=100
+readonly R_IP_TALKER="10.1.0.1"
+readonly R_IP_LISTENER="10.1.0.2"
+
+# FRER action index constants
+readonly IDX_PUSH=1
+readonly IDX_SHARED_RCVY=10
+readonly IDX_INDV_RCVY_A=20
+readonly IDX_INDV_RCVY_B=21
+readonly IDX_NO_POP=30
+readonly IDX_P2P_RCVY=40
+readonly IDX_RELAY_PUSH=50
+readonly IDX_RELAY_RCVY=60
+
+readonly NUM_TESTS=6
+
+# ----------------------------------------------------------------------------
+# Prerequisite check
+# ----------------------------------------------------------------------------
+check_prerequisites()
+{
+	local missing=0
+
+	[ "$(id -u)" -eq 0 ] || { echo "# Must be run as root" >&2; missing=1; }
+
+	for cmd in ip "$TC" "$TCPDUMP" "$PING"; do
+		command -v "$cmd" >/dev/null 2>&1 || {
+			echo "# Missing command: $cmd" >&2
+			missing=1
+		}
+	done
+
+	if [ "$missing" -ne 0 ]; then
+		ksft_set_plan "$NUM_TESTS"
+		for i in $(seq 1 "$NUM_TESTS"); do
+			ksft_test_result_skip "prerequisites not met (test $i)"
+		done
+		ksft_print_cnts
+		exit "$KSFT_SKIP"
+	fi
+}
+
+load_module()
+{
+	[ "$SKIP_MODPROBE" = "1" ] && return
+	if ! modprobe act_frer 2>/dev/null; then
+		echo "# modprobe act_frer failed - may be built-in or unavailable" >&2
+	fi
+	if ! modprobe bonding 2>/dev/null; then
+		echo "# modprobe bonding failed - may be built-in or unavailable" >&2
+	fi
+}
+
+check_frer_action()
+{
+	ip netns exec "$NS_TALKER" \
+		$TC actions add action frer push index 999 2>/dev/null || return 1
+	ip netns exec "$NS_TALKER" \
+		$TC actions del action frer index 999 2>/dev/null || true
+	return 0
+}
+
+# ----------------------------------------------------------------------------
+# Bond topology setup / teardown (used by tests 1-4)
+# ----------------------------------------------------------------------------
+setup_topology()
+{
+	for n in "$NS_TALKER" "$NS_LISTENER"; do
+		ip netns add "$n"
+	done
+
+	ip link add "$VETH_A0" type veth peer name "$VETH_A1"
+	ip link set "$VETH_A0" netns "$NS_TALKER"
+	ip link set "$VETH_A1" netns "$NS_LISTENER"
+
+	ip link add "$VETH_B0" type veth peer name "$VETH_B1"
+	ip link set "$VETH_B0" netns "$NS_TALKER"
+	ip link set "$VETH_B1" netns "$NS_LISTENER"
+
+	# ns_talker: create bond0 (balance-rr), frames round-robin across both slaves.
+	ip netns exec "$NS_TALKER" ip link set lo up
+	ip netns exec "$NS_TALKER" ip link add "$BOND0" type bond mode balance-rr miimon 100
+	ip netns exec "$NS_TALKER" ip link set "$VETH_A0" master "$BOND0"
+	ip netns exec "$NS_TALKER" ip link set "$VETH_B0" master "$BOND0"
+	ip netns exec "$NS_TALKER" ip link set "$VETH_A0" up
+	ip netns exec "$NS_TALKER" ip link set "$VETH_B0" up
+	ip netns exec "$NS_TALKER" ip link set "$BOND0" up
+	ip netns exec "$NS_TALKER" ip addr add "${IP_SRC}/24" dev "$BOND0"
+
+	# ns_listener: create bond1 (balance-rr).
+	ip netns exec "$NS_LISTENER" ip link set lo up
+	ip netns exec "$NS_LISTENER" ip link add "$BOND1" type bond mode balance-rr miimon 100
+	ip netns exec "$NS_LISTENER" ip link set "$VETH_A1" master "$BOND1"
+	ip netns exec "$NS_LISTENER" ip link set "$VETH_B1" master "$BOND1"
+	ip netns exec "$NS_LISTENER" ip link set "$VETH_A1" up
+	ip netns exec "$NS_LISTENER" ip link set "$VETH_B1" up
+	ip netns exec "$NS_LISTENER" ip link set "$BOND1" up
+	ip netns exec "$NS_LISTENER" ip addr add "${IP_DST}/24" dev "$BOND1"
+
+	# Static ARP so L2 forwarding works without ARP broadcasts.
+	# With balance-rr both slaves share the bond MAC.
+	local mac_bond0 mac_bond1
+	mac_bond0=$(ip netns exec "$NS_TALKER"   cat /sys/class/net/"$BOND0"/address)
+	mac_bond1=$(ip netns exec "$NS_LISTENER" cat /sys/class/net/"$BOND1"/address)
+	ip netns exec "$NS_TALKER"   ip neigh add "$IP_DST" lladdr "$mac_bond1" dev "$BOND0"
+	ip netns exec "$NS_LISTENER" ip neigh add "$IP_SRC" lladdr "$mac_bond0" dev "$BOND1"
+}
+
+cleanup()
+{
+	for n in "$NS_TALKER" "$NS_LISTENER" \
+		"$P2P_NS_SRC" "$P2P_NS_DST" \
+		"$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+		ip netns del "$n" 2>/dev/null || true
+	done
+}
+trap cleanup EXIT
+
+# ----------------------------------------------------------------------------
+# TC rule helpers
+# ----------------------------------------------------------------------------
+
+# Push on both veth_a0 and veth_b0 egress using the same shared frer push
+# action (IDX_PUSH).  Each slave also mirrors to the other so that every
+# outgoing frame is replicated onto both paths regardless of which slave the
+# bond currently selects.  This prevents packet loss during bond link changes.
+setup_push_mirror()
+{
+	ip netns exec "$NS_TALKER" $TC qdisc add dev "$VETH_A0" clsact
+	ip netns exec "$NS_TALKER" $TC filter add dev "$VETH_A0" egress \
+		protocol ip flower skip_hw \
+		action frer push index $IDX_PUSH \
+		action mirred egress mirror dev "$VETH_B0"
+
+	ip netns exec "$NS_TALKER" $TC qdisc add dev "$VETH_B0" clsact
+	ip netns exec "$NS_TALKER" $TC filter add dev "$VETH_B0" egress \
+		protocol ip flower skip_hw \
+		action frer push index $IDX_PUSH \
+		action mirred egress mirror dev "$VETH_A0"
+}
+
+teardown_tc()
+{
+	for dev in "$VETH_A0" "$VETH_B0"; do
+		ip netns exec "$NS_TALKER" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	for dev in "$VETH_A1" "$VETH_B1"; do
+		ip netns exec "$NS_LISTENER" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	ip netns exec "$NS_TALKER"   $TC actions flush action frer 2>/dev/null || true
+	ip netns exec "$NS_LISTENER" $TC actions flush action frer 2>/dev/null || true
+}
+
+# ----------------------------------------------------------------------------
+# Packet-capture helpers
+#
+# capture_start_on NS IFACE PCAP [BPF_FILTER]
+#   Starts tcpdump in namespace NS on IFACE, writing to PCAP.
+#   Stores PID in _CAP_PID.
+#
+# capture_stop
+#   Waits for tcpdump (stored in _CAP_PID) to finish.
+#
+# capture_count_on NS PCAP
+#   Prints the number of captured packets.
+#
+# Convenience wrappers capture_start / capture_count target bond1 in
+# NS_LISTENER (the primary observation point for tests 2 and 4).
+# ----------------------------------------------------------------------------
+_CAP_PID=""
+
+capture_start_on()
+{
+	local ns="$1" iface="$2" pcap="$3" filter="${4:-}"
+
+	if [ -n "$filter" ]; then
+		ip netns exec "$ns" timeout 4 \
+			$TCPDUMP -i "$iface" -w "$pcap" \
+			--immediate-mode -Z root -y EN10MB \
+			$filter >/dev/null 2>&1 &
+	else
+		ip netns exec "$ns" timeout 4 \
+			$TCPDUMP -i "$iface" -w "$pcap" \
+			--immediate-mode -Z root -y EN10MB \
+			>/dev/null 2>&1 &
+	fi
+	_CAP_PID=$!
+
+	# Wait until tcpdump opens a packet socket (max ~2.5 s).
+	local tries=0
+	while [ $tries -lt 50 ]; do
+		ip netns exec "$ns" grep -q "$iface" /proc/net/packet 2>/dev/null && break
+		sleep 0.05
+		tries=$((tries + 1))
+	done
+}
+
+capture_stop()
+{
+	[ -n "$_CAP_PID" ] || return 0
+	wait "$_CAP_PID" 2>/dev/null || true
+	_CAP_PID=""
+}
+
+capture_count_on()
+{
+	local ns="$1" pcap="$2"
+	ip netns exec "$ns" \
+		$TCPDUMP -r "$pcap" --no-promiscuous-mode 2>/dev/null \
+		| grep -c "^[0-9]" || true
+}
+
+# Convenience wrappers: default to bond1 in NS_LISTENER
+capture_start() { capture_start_on "$NS_LISTENER" "$BOND1" "$@"; }
+capture_count() { capture_count_on "$NS_LISTENER" "$1"; }
+
+# ----------------------------------------------------------------------------
+# Ping helper
+# ----------------------------------------------------------------------------
+do_ping()
+{
+	local rc=0
+	ip netns exec "$NS_TALKER" \
+		$PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+		"$IP_DST" >/dev/null 2>&1 || rc=$?
+	return $rc
+}
+
+# ----------------------------------------------------------------------------
+# tc statistics parser
+# ----------------------------------------------------------------------------
+tc_stat()
+{
+	local dump="$1" field="$2"
+	echo "$dump" | awk -F"${field}=" 'NF>1{split($2,a," ");print a[1];exit}' || echo "0"
+}
+
+# ----------------------------------------------------------------------------
+# TEST 1: PUSH VERIFY (bond topology)
+#
+# Only push is configured on the talker side; no recover on the listener.
+# The push action on veth_a0 egress inserts an R-TAG and mirrors a copy to
+# veth_b0, so both listener slaves (veth_a1 and veth_b1) receive a frame
+# with EtherType 0xF1C1.  Captures run sequentially on each slave to verify
+# that both paths carry R-TAG frames.
+#
+# Pass criteria:
+#   - veth_a1 captures >= 1 R-TAG frame
+#   - veth_b1 captures >= 1 R-TAG frame
+# ----------------------------------------------------------------------------
+test_push_verify_bond()
+{
+	local pcap_a pcap_b cap_a cap_b
+	local result="pass"
+
+	setup_push_mirror
+
+	# Capture 1: R-TAG frames on veth_a1 (path A)
+	pcap_a=$(mktemp /tmp/frer_bond_push_a_XXXXXX.pcap)
+	capture_start_on "$NS_LISTENER" "$VETH_A1" "$pcap_a" "ether proto 0xf1c1"
+	ip netns exec "$NS_TALKER" \
+		$PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+	capture_stop
+	cap_a=$(capture_count_on "$NS_LISTENER" "$pcap_a")
+	rm -f "$pcap_a"
+
+	# Capture 2: R-TAG frames on veth_b1 (path B, mirrored copy)
+	pcap_b=$(mktemp /tmp/frer_bond_push_b_XXXXXX.pcap)
+	capture_start_on "$NS_LISTENER" "$VETH_B1" "$pcap_b" "ether proto 0xf1c1"
+	ip netns exec "$NS_TALKER" \
+		$PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+	capture_stop
+	cap_b=$(capture_count_on "$NS_LISTENER" "$pcap_b")
+	rm -f "$pcap_b"
+
+	teardown_tc
+
+	echo "# bond push verify: veth_a1 R-TAG=$cap_a veth_b1 R-TAG=$cap_b"
+
+	[ "$cap_a" -ge 1 ] || result="fail"
+	[ "$cap_b" -ge 1 ] || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"bond push verify: R-TAG on both paths (a1=$cap_a b1=$cap_b)"
+	else
+		ksft_test_result_fail \
+			"bond push verify: expected R-TAG on both paths (a1=$cap_a b1=$cap_b)"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 2: SHARED RECOVER E2E (bond topology)
+#
+# veth_a1 and veth_b1 ingress share one recover action (idx=10) with tag-pop.
+# The listener receives two R-TAG copies per request; the shared recover passes
+# exactly one and discards the other.  The recovered plain ICMP reaches bond1's
+# IP stack and a reply is sent, making ping succeed.
+#
+# Pass criteria:
+#   - ping succeeds (rc=0)
+#   - tcpdump on bond1 captures exactly PING_COUNT ICMP echo-request frames
+#     (filter is restricted to type=8 to exclude echo replies, which would
+#     double the count since bond1 also originates the reply packets)
+#   - tc stats on veth_a1: passed >= PING_COUNT, discarded >= PING_COUNT
+# ----------------------------------------------------------------------------
+test_shared_recover_bond()
+{
+	local pcap cap_count ping_rc=0
+	local dump_a
+	local total_passed total_discarded tagless
+	local result="pass"
+
+	setup_push_mirror
+
+	# veth_a1 ingress: create shared recover action with tag-pop
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+		protocol all flower skip_hw \
+		action frer recover alg vector history-length 16 \
+			reset-time 2000 tag-pop index $IDX_SHARED_RCVY
+
+	# veth_b1 ingress: bind to the same shared action by index
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+		protocol all flower skip_hw \
+		action frer recover index $IDX_SHARED_RCVY
+
+	pcap=$(mktemp /tmp/frer_bond_shared_XXXXXX.pcap)
+	capture_start "$pcap" "icmp[icmptype] == icmp-echo"
+
+	do_ping || ping_rc=$?
+
+	capture_stop
+
+	cap_count=$(capture_count "$pcap")
+	rm -f "$pcap"
+
+	dump_a=$(ip netns exec "$NS_LISTENER" \
+		$TC -s filter show dev "$VETH_A1" ingress 2>/dev/null)
+
+	teardown_tc
+
+	total_passed=$(tc_stat    "$dump_a" "passed")
+	total_discarded=$(tc_stat "$dump_a" "discarded")
+	tagless=$(tc_stat         "$dump_a" "tagless")
+	total_discarded=$((total_discarded - tagless))
+
+	echo "# bond shared recover: ping_rc=$ping_rc cap=$cap_count" \
+		"passed=$total_passed discarded=$total_discarded"
+
+	[ "$ping_rc"         -eq 0 ]            || result="fail"
+	[ "$cap_count"       -eq "$PING_COUNT" ] || result="fail"
+	[ "$total_passed"    -ge "$PING_COUNT" ] || result="fail"
+	[ "$total_discarded" -ge "$PING_COUNT" ] || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"bond shared recover: ping OK, cap=$cap_count" \
+			"passed=$total_passed discarded=$total_discarded"
+	else
+		ksft_test_result_fail \
+			"bond shared recover: ping_rc=$ping_rc cap=$cap_count" \
+			"passed=$total_passed discarded=$total_discarded" \
+			"(expected ping OK, cap=$PING_COUNT," \
+			"passed>=$PING_COUNT, discarded>=$PING_COUNT)"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 3: INDIVIDUAL RECOVER (bond topology)
+#
+# veth_a1 and veth_b1 use independent recover actions (idx=20 and idx=21).
+# Each port maintains its own sequence history so both copies of every frame
+# are passed (no cross-port deduplication).  With active-backup bond1, only
+# the active slave's (veth_a1) recovered frame reaches bond1's IP stack, so
+# ping succeeds.  The absence of deduplication is verified via per-slave
+# tcpdump (each slave should capture PING_COUNT ICMP frames) and tc stats.
+#
+# Pass criteria:
+#   - ping succeeds
+#   - veth_a1 captures PING_COUNT ICMP frames (passed, not discarded)
+#   - veth_b1 captures PING_COUNT ICMP frames (passed independently)
+#   - tc stats: veth_a1 passed=PING_COUNT discarded=0
+#               veth_b1 passed=PING_COUNT discarded=0
+# ----------------------------------------------------------------------------
+test_individual_recover_bond()
+{
+	local pcap_a pcap_b cap_a cap_b ping_rc=0
+	local dump_a dump_b
+	local passed_a discarded_a passed_b discarded_b tagless_a tagless_b
+	local result="pass"
+
+	setup_push_mirror
+
+	# veth_a1 ingress: individual recover idx=20 (independent state)
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+		protocol all flower skip_hw \
+		action frer recover individual alg vector history-length 16 \
+			reset-time 2000 tag-pop index $IDX_INDV_RCVY_A
+
+	# veth_b1 ingress: individual recover idx=21 (separate independent state)
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+		protocol all flower skip_hw \
+		action frer recover individual alg vector history-length 16 \
+			reset-time 2000 tag-pop index $IDX_INDV_RCVY_B
+
+	# Per-slave capture A: verify veth_a1 passes frames; also use this run
+	# for the overall ping_rc check (do_ping targets bond0->bond1).
+	pcap_a=$(mktemp /tmp/frer_bond_indv_a_XXXXXX.pcap)
+	capture_start_on "$NS_LISTENER" "$VETH_A1" "$pcap_a" "icmp"
+	do_ping || ping_rc=$?
+	capture_stop
+	cap_a=$(capture_count_on "$NS_LISTENER" "$pcap_a")
+	rm -f "$pcap_a"
+
+	# Per-slave capture B: verify veth_b1 also passes frames (balance-rr
+	# distributes egress across both slaves, so both paths carry traffic).
+	pcap_b=$(mktemp /tmp/frer_bond_indv_b_XXXXXX.pcap)
+	capture_start_on "$NS_LISTENER" "$VETH_B1" "$pcap_b" "icmp"
+	do_ping || true
+	capture_stop
+	cap_b=$(capture_count_on "$NS_LISTENER" "$pcap_b")
+	rm -f "$pcap_b"
+
+	dump_a=$(ip netns exec "$NS_LISTENER" \
+		$TC -s filter show dev "$VETH_A1" ingress 2>/dev/null)
+	dump_b=$(ip netns exec "$NS_LISTENER" \
+		$TC -s filter show dev "$VETH_B1" ingress 2>/dev/null)
+
+	teardown_tc
+
+	passed_a=$(tc_stat    "$dump_a" "passed")
+	discarded_a=$(tc_stat "$dump_a" "discarded")
+	tagless_a=$(tc_stat   "$dump_a" "tagless")
+	passed_b=$(tc_stat    "$dump_b" "passed")
+	discarded_b=$(tc_stat "$dump_b" "discarded")
+	tagless_b=$(tc_stat   "$dump_b" "tagless")
+	discarded_a=$((discarded_a - tagless_a))
+	discarded_b=$((discarded_b - tagless_b))
+
+	echo "# bond individual recover: ping_rc=$ping_rc" \
+		"a1: cap=$cap_a passed=$passed_a discarded=$discarded_a" \
+		"b1: cap=$cap_b passed=$passed_b discarded=$discarded_b"
+
+	[ "$ping_rc"   -eq 0 ]            || result="fail"
+	[ "$cap_a"     -ge "$PING_COUNT" ] || result="fail"
+	[ "$cap_b"     -ge "$PING_COUNT" ] || result="fail"
+	[ "$passed_a"  -ge "$PING_COUNT" ] || result="fail"
+	[ "$passed_b"  -ge "$PING_COUNT" ] || result="fail"
+	[ "$discarded_a" -eq 0 ]           || result="fail"
+	[ "$discarded_b" -eq 0 ]           || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"bond individual recover: ping OK" \
+			"a1: cap=$cap_a passed=$passed_a/0" \
+			"b1: cap=$cap_b passed=$passed_b/0"
+	else
+		ksft_test_result_fail \
+			"bond individual recover: ping_rc=$ping_rc" \
+			"a1: cap=$cap_a passed=$passed_a discarded=$discarded_a" \
+			"b1: cap=$cap_b passed=$passed_b discarded=$discarded_b"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 4: NO TAG-POP (bond topology)
+#
+# Shared recover runs without tag-pop; passed frames still carry the R-TAG
+# when they reach bond1.
+#
+# Pass criteria:
+#   - tcpdump on bond1 with "ether proto 0xf1c1" captures >= 1 R-TAG frame
+#   - tcpdump on bond1 with "icmp" captures 0 frames (outer EtherType is
+#     0xF1C1, not 0x0800, so plain-IP ICMP filter does not match)
+# ----------------------------------------------------------------------------
+test_no_tag_pop_bond()
+{
+	local pcap_rtag pcap_icmp rtag_count icmp_count
+	local result="pass"
+
+	setup_push_mirror
+
+	# veth_a1 ingress: shared recover WITHOUT tag-pop
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_A1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_A1" ingress \
+		protocol all flower skip_hw \
+		action frer recover alg vector history-length 16 \
+			reset-time 2000 index $IDX_NO_POP
+
+	# veth_b1 ingress: bind to the same shared action
+	ip netns exec "$NS_LISTENER" $TC qdisc add dev "$VETH_B1" clsact
+	ip netns exec "$NS_LISTENER" $TC filter add dev "$VETH_B1" ingress \
+		protocol all flower skip_hw \
+		action frer recover index $IDX_NO_POP
+
+	# Capture 1: frames with R-TAG EtherType on bond1 (expect >= 1)
+	pcap_rtag=$(mktemp /tmp/frer_bond_nopop_rtag_XXXXXX.pcap)
+	capture_start "$pcap_rtag" "ether proto 0xf1c1"
+	ip netns exec "$NS_TALKER" \
+		$PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+	capture_stop
+	rtag_count=$(capture_count "$pcap_rtag")
+	rm -f "$pcap_rtag"
+
+	# Capture 2: plain ICMP frames on bond1 (expect 0)
+	pcap_icmp=$(mktemp /tmp/frer_bond_nopop_icmp_XXXXXX.pcap)
+	capture_start "$pcap_icmp" "icmp"
+	ip netns exec "$NS_TALKER" \
+		$PING -c 3 -W 1 -i 0.2 -q "$IP_DST" >/dev/null 2>&1 || true
+	capture_stop
+	icmp_count=$(capture_count "$pcap_icmp")
+	rm -f "$pcap_icmp"
+
+	teardown_tc
+
+	echo "# bond no tag-pop: rtag=$rtag_count (expected >=1) icmp=$icmp_count (expected 0)"
+
+	[ "$rtag_count" -ge 1 ] || result="fail"
+	[ "$icmp_count" -eq 0 ] || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"bond no tag-pop: R-TAG present on bond1 " \
+			"(rtag=$rtag_count), ICMP absent (icmp=$icmp_count)"
+	else
+		ksft_test_result_fail \
+			"bond no tag-pop: rtag=$rtag_count icmp=$icmp_count " \
+			"(expected rtag>=1 icmp=0)"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 5: SIMPLE POINT-TO-POINT (no bond)
+#
+# Self-contained single-path topology: push on p2p_a0 egress, individual
+# recover (with tag-pop) on p2p_a1 ingress.  IP is assigned directly to the
+# veth interfaces (no bond).
+#
+# Pass criteria:
+#   - ping succeeds (rc=0)
+#   - veth_a1 recover stats: passed >= PING_COUNT, discarded = 0
+# ----------------------------------------------------------------------------
+test_simple_point_to_point()
+{
+	local ping_rc=0
+	local dump_a1 passed discarded
+	local result="pass"
+
+	# Create self-contained p2p namespaces
+	ip netns add "$P2P_NS_SRC"
+	ip netns add "$P2P_NS_DST"
+
+	ip link add "$P2P_VETH_A0" type veth peer name "$P2P_VETH_A1"
+	ip link set "$P2P_VETH_A0" netns "$P2P_NS_SRC"
+	ip link set "$P2P_VETH_A1" netns "$P2P_NS_DST"
+
+	ip netns exec "$P2P_NS_SRC" ip link set lo up
+	ip netns exec "$P2P_NS_SRC" ip link set "$P2P_VETH_A0" up
+	ip netns exec "$P2P_NS_SRC" ip addr add "${IP_P2P_SRC}/24" dev "$P2P_VETH_A0"
+
+	ip netns exec "$P2P_NS_DST" ip link set lo up
+	ip netns exec "$P2P_NS_DST" ip link set "$P2P_VETH_A1" up
+	ip netns exec "$P2P_NS_DST" ip addr add "${IP_P2P_DST}/24" dev "$P2P_VETH_A1"
+
+	local mac_a0 mac_a1
+	mac_a0=$(ip netns exec "$P2P_NS_SRC" cat /sys/class/net/"$P2P_VETH_A0"/address)
+	mac_a1=$(ip netns exec "$P2P_NS_DST" cat /sys/class/net/"$P2P_VETH_A1"/address)
+	ip netns exec "$P2P_NS_SRC" ip neigh add "$IP_P2P_DST" lladdr "$mac_a1" dev "$P2P_VETH_A0"
+	ip netns exec "$P2P_NS_DST" ip neigh add "$IP_P2P_SRC" lladdr "$mac_a0" dev "$P2P_VETH_A1"
+
+	# veth_a0 egress: push R-TAG
+	ip netns exec "$P2P_NS_SRC" $TC qdisc add dev "$P2P_VETH_A0" clsact
+	ip netns exec "$P2P_NS_SRC" $TC filter add dev "$P2P_VETH_A0" egress \
+		protocol ip flower skip_hw \
+		action frer push index $IDX_PUSH
+
+	# veth_a1 ingress: individual recover with tag-pop
+	ip netns exec "$P2P_NS_DST" $TC qdisc add dev "$P2P_VETH_A1" clsact
+	ip netns exec "$P2P_NS_DST" $TC filter add dev "$P2P_VETH_A1" ingress \
+		protocol all flower skip_hw \
+		action frer recover individual alg vector history-length 16 \
+			reset-time 2000 tag-pop index $IDX_P2P_RCVY
+
+	ip netns exec "$P2P_NS_SRC" \
+		$PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+		"$IP_P2P_DST" >/dev/null 2>&1 || ping_rc=$?
+
+	dump_a1=$(ip netns exec "$P2P_NS_DST" \
+		$TC -s filter show dev "$P2P_VETH_A1" ingress 2>/dev/null)
+
+	# Teardown p2p topology
+	for dev in "$P2P_VETH_A0"; do
+		ip netns exec "$P2P_NS_SRC" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	for dev in "$P2P_VETH_A1"; do
+		ip netns exec "$P2P_NS_DST" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	ip netns exec "$P2P_NS_SRC" $TC actions flush action frer 2>/dev/null || true
+	ip netns exec "$P2P_NS_DST" $TC actions flush action frer 2>/dev/null || true
+	ip netns del "$P2P_NS_SRC" 2>/dev/null || true
+	ip netns del "$P2P_NS_DST" 2>/dev/null || true
+
+	passed=$(tc_stat    "$dump_a1" "passed")
+	discarded=$(tc_stat "$dump_a1" "discarded")
+	local tagless
+	tagless=$(tc_stat   "$dump_a1" "tagless")
+	discarded=$((discarded - tagless))
+
+	echo "# p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded"
+
+	[ "$ping_rc"   -eq 0 ]            || result="fail"
+	[ "$passed"    -ge "$PING_COUNT" ] || result="fail"
+	[ "$discarded" -eq 0 ]            || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"simple p2p: ping OK, passed=$passed discarded=$discarded"
+	else
+		ksft_test_result_fail \
+			"simple p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# TEST 6: RELAY E2E (self-contained, no bond)
+#
+# Talker sends VLAN-100 frames into bridge0 (sequence generator).  Bridge0
+# pushes an R-TAG and replicates to two redundant paths.  Bridge1 (eliminator)
+# recovers (shared, tag-pop) on both paths and forwards the deduplicated frame
+# to the listener.
+#
+# Topology:
+#   ns_talker (talker_eth.100) -- talker_eth/br0_uplink
+#       -- bridge0 (br_r0) -+- br0_swp0/br1_swp0 -+
+#                            \- br0_swp1/br1_swp1 -+
+#       -- bridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener
+#
+# FRER rules:
+#   bridge0 / br0_uplink ingress  : push idx=50, redirect br0_swp0, mirror br0_swp1
+#   bridge1 / br1_swp0 ingress    : recover (shared, tag-pop) idx=60, redirect br1_downlink
+#   bridge1 / br1_swp1 ingress    : recover idx=60 (bind same), redirect br1_downlink
+#   bridge1 / br1_downlink ingress: redirect br1_swp0 (reply path, bypass FDB)
+#
+# Pass criteria:
+#   - ping from ns_talker to ns_listener succeeds (rc=0)
+#   - tcpdump on listener captures exactly PING_COUNT ICMP echo-request frames
+#   - br1_swp0 tc stats: passed >= PING_COUNT, discarded >= PING_COUNT
+# ----------------------------------------------------------------------------
+teardown_relay_tc()
+{
+	for dev in "$R_BR0_UPLINK"; do
+		ip netns exec "$R_NS_BRIDGE0" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	for dev in "$R_BR1_SWP0" "$R_BR1_SWP1" "$R_BR1_DOWNLINK"; do
+		ip netns exec "$R_NS_BRIDGE1" $TC qdisc del dev "$dev" clsact \
+			2>/dev/null || true
+	done
+	ip netns exec "$R_NS_BRIDGE0" $TC actions flush action frer 2>/dev/null || true
+	ip netns exec "$R_NS_BRIDGE1" $TC actions flush action frer 2>/dev/null || true
+}
+
+test_relay_e2e()
+{
+	local ping_rc=0
+	local dump_r1swp0
+	local total_passed total_discarded
+	local result="pass"
+	local ns
+
+	for ns in "$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+		ip netns add "$ns" || {
+			echo "# relay e2e: failed to create netns $ns" >&2
+			ksft_test_result_skip "relay e2e: netns setup failed"
+			return
+		}
+	done
+
+	ip link add "$R_TALKER_ETH"   type veth peer name "$R_BR0_UPLINK"
+	ip link add "$R_BR0_SWP0"     type veth peer name "$R_BR1_SWP0"
+	ip link add "$R_BR0_SWP1"     type veth peer name "$R_BR1_SWP1"
+	ip link add "$R_BR1_DOWNLINK" type veth peer name "$R_LISTENER_ETH"
+
+	ip link set "$R_TALKER_ETH"   netns "$R_NS_TALKER"
+	ip link set "$R_BR0_UPLINK"   netns "$R_NS_BRIDGE0"
+	ip link set "$R_BR0_SWP0"     netns "$R_NS_BRIDGE0"
+	ip link set "$R_BR0_SWP1"     netns "$R_NS_BRIDGE0"
+	ip link set "$R_BR1_SWP0"     netns "$R_NS_BRIDGE1"
+	ip link set "$R_BR1_SWP1"     netns "$R_NS_BRIDGE1"
+	ip link set "$R_BR1_DOWNLINK" netns "$R_NS_BRIDGE1"
+	ip link set "$R_LISTENER_ETH" netns "$R_NS_LISTENER"
+
+	local ns_dev
+	for ns_dev in \
+		"$R_NS_TALKER:$R_TALKER_ETH" \
+		"$R_NS_BRIDGE0:$R_BR0_UPLINK" "$R_NS_BRIDGE0:$R_BR0_SWP0" \
+		"$R_NS_BRIDGE0:$R_BR0_SWP1" \
+		"$R_NS_BRIDGE1:$R_BR1_SWP0" "$R_NS_BRIDGE1:$R_BR1_SWP1" \
+		"$R_NS_BRIDGE1:$R_BR1_DOWNLINK" \
+		"$R_NS_LISTENER:$R_LISTENER_ETH"; do
+		local _ns="${ns_dev%%:*}"
+		local _dev="${ns_dev##*:}"
+		ip netns exec "$_ns" ip link set lo up
+		ip netns exec "$_ns" ip link set "$_dev" up
+	done
+
+	# bridge0: sequence generator, VLAN filtering
+	ip netns exec "$R_NS_BRIDGE0" ip link add name "$R_BR0" type bridge vlan_filtering 1
+	ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0" up
+	ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_UPLINK" master "$R_BR0"
+	ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_SWP0" master "$R_BR0"
+	ip netns exec "$R_NS_BRIDGE0" ip link set "$R_BR0_SWP1" master "$R_BR0"
+
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_UPLINK" vid "$R_VLAN"
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_SWP0" vid "$R_VLAN"
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan del dev "$R_BR0_SWP1" vid 1
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan add dev "$R_BR0_SWP1" \
+		vid "$R_VLAN" pvid untagged
+	ip netns exec "$R_NS_BRIDGE0" bridge link set dev "$R_BR0_SWP0" learning off
+	ip netns exec "$R_NS_BRIDGE0" bridge link set dev "$R_BR0_SWP1" learning off
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan set dev "$R_BR0_SWP0" vid "$R_VLAN" noflood
+	ip netns exec "$R_NS_BRIDGE0" bridge vlan set dev "$R_BR0_SWP1" vid "$R_VLAN" noflood
+
+	# bridge1: eliminator, VLAN filtering
+	ip netns exec "$R_NS_BRIDGE1" ip link add name "$R_BR1" type bridge vlan_filtering 1
+	ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1" up
+	ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_SWP0" master "$R_BR1"
+	ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_SWP1" master "$R_BR1"
+	ip netns exec "$R_NS_BRIDGE1" ip link set "$R_BR1_DOWNLINK" master "$R_BR1"
+
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_SWP0" vid "$R_VLAN"
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan del dev "$R_BR1_SWP1" vid 1
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_SWP1" \
+		vid "$R_VLAN" pvid untagged
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan add dev "$R_BR1_DOWNLINK" vid "$R_VLAN"
+	ip netns exec "$R_NS_BRIDGE1" bridge link set dev "$R_BR1_SWP0" learning off
+	ip netns exec "$R_NS_BRIDGE1" bridge link set dev "$R_BR1_SWP1" learning off
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan set dev "$R_BR1_SWP0" vid "$R_VLAN" noflood
+	ip netns exec "$R_NS_BRIDGE1" bridge vlan set dev "$R_BR1_SWP1" vid "$R_VLAN" noflood
+
+	# ns_talker: VLAN sub-interface
+	ip netns exec "$R_NS_TALKER" ip link add link "$R_TALKER_ETH" \
+		name "${R_TALKER_ETH}.${R_VLAN}" type vlan id "$R_VLAN"
+	ip netns exec "$R_NS_TALKER" ip link set "${R_TALKER_ETH}.${R_VLAN}" up
+	ip netns exec "$R_NS_TALKER" ip addr add "${R_IP_TALKER}/24" \
+		dev "${R_TALKER_ETH}.${R_VLAN}"
+
+	# ns_listener: VLAN sub-interface
+	ip netns exec "$R_NS_LISTENER" ip link add link "$R_LISTENER_ETH" \
+		name "${R_LISTENER_ETH}.${R_VLAN}" type vlan id "$R_VLAN"
+	ip netns exec "$R_NS_LISTENER" ip link set "${R_LISTENER_ETH}.${R_VLAN}" up
+	ip netns exec "$R_NS_LISTENER" ip addr add "${R_IP_LISTENER}/24" \
+		dev "${R_LISTENER_ETH}.${R_VLAN}"
+
+	# Static ARP (VLAN 100 flooding is disabled)
+	local mac_talker mac_listener
+	mac_talker=$(ip netns exec "$R_NS_TALKER" \
+		cat /sys/class/net/"${R_TALKER_ETH}.${R_VLAN}"/address)
+	mac_listener=$(ip netns exec "$R_NS_LISTENER" \
+		cat /sys/class/net/"${R_LISTENER_ETH}.${R_VLAN}"/address)
+	ip netns exec "$R_NS_TALKER"   ip neigh add "$R_IP_LISTENER" \
+		lladdr "$mac_listener" dev "${R_TALKER_ETH}.${R_VLAN}"
+	ip netns exec "$R_NS_LISTENER" ip neigh add "$R_IP_TALKER" \
+		lladdr "$mac_talker"   dev "${R_LISTENER_ETH}.${R_VLAN}"
+
+	# bridge0 / br0_uplink ingress: push R-TAG then replicate to both redundant paths.
+	# mirror must come before redirect because redirect is a terminating action.
+	ip netns exec "$R_NS_BRIDGE0" $TC qdisc add dev "$R_BR0_UPLINK" clsact
+	ip netns exec "$R_NS_BRIDGE0" $TC filter add dev "$R_BR0_UPLINK" ingress \
+		protocol 802.1Q flower skip_hw vlan_id "$R_VLAN" \
+		action frer push index $IDX_RELAY_PUSH \
+		action mirred egress mirror  dev "$R_BR0_SWP1" \
+		action mirred egress redirect dev "$R_BR0_SWP0"
+
+	# bridge1 / br1_swp0 ingress: create shared recover action (tag-pop)
+	ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_SWP0" clsact
+	ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_SWP0" ingress \
+		protocol all flower skip_hw \
+		action frer recover alg vector history-length 16 \
+			reset-time 2000 tag-pop index $IDX_RELAY_RCVY \
+		action mirred egress redirect dev "$R_BR1_DOWNLINK"
+
+	# bridge1 / br1_swp1 ingress: bind to the same shared recover action
+	ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_SWP1" clsact
+	ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_SWP1" ingress \
+		protocol all flower skip_hw \
+		action frer recover index $IDX_RELAY_RCVY \
+		action mirred egress redirect dev "$R_BR1_DOWNLINK"
+
+	# bridge1 / br1_downlink ingress: redirect VLAN 100 replies directly to br1_swp0
+	ip netns exec "$R_NS_BRIDGE1" $TC qdisc add dev "$R_BR1_DOWNLINK" clsact
+	ip netns exec "$R_NS_BRIDGE1" $TC filter add dev "$R_BR1_DOWNLINK" ingress \
+		protocol 802.1Q flower skip_hw vlan_id "$R_VLAN" \
+		action mirred egress redirect dev "$R_BR1_SWP0"
+
+	# Capture ICMP echo-requests on listener_eth.VLAN to verify exactly
+	# PING_COUNT deduplicated frames reach the listener after recovery.
+	local pcap cap_count
+	pcap=$(mktemp /tmp/frer_relay_XXXXXX.pcap)
+	capture_start_on "$R_NS_LISTENER" "${R_LISTENER_ETH}.${R_VLAN}" \
+		"$pcap" "icmp[icmptype] == icmp-echo"
+
+	ip netns exec "$R_NS_TALKER" \
+		$PING -c "$PING_COUNT" -W "$PING_TIMEOUT" -i 0.2 -q \
+		"$R_IP_LISTENER" >/dev/null 2>&1 || ping_rc=$?
+
+	capture_stop
+	cap_count=$(capture_count_on "$R_NS_LISTENER" "$pcap")
+	rm -f "$pcap"
+
+	dump_br1_swp0=$(ip netns exec "$R_NS_BRIDGE1" \
+		$TC -s filter show dev "$R_BR1_SWP0" ingress 2>/dev/null)
+
+	teardown_relay_tc
+	for ns in "$R_NS_TALKER" "$R_NS_BRIDGE0" "$R_NS_BRIDGE1" "$R_NS_LISTENER"; do
+		ip netns del "$ns" 2>/dev/null || true
+	done
+
+	total_passed=$(tc_stat    "$dump_br1_swp0" "passed")
+	total_discarded=$(tc_stat "$dump_br1_swp0" "discarded")
+	local tagless
+	tagless=$(tc_stat         "$dump_br1_swp0" "tagless")
+	total_discarded=$((total_discarded - tagless))
+
+	echo "# relay e2e: ping_rc=$ping_rc cap=$cap_count" \
+		"passed=$total_passed discarded=$total_discarded"
+
+	[ "$ping_rc"         -eq 0 ]            || result="fail"
+	[ "$cap_count"       -eq "$PING_COUNT" ] || result="fail"
+	[ "$total_passed"    -ge "$PING_COUNT" ] || result="fail"
+	[ "$total_discarded" -ge "$PING_COUNT" ] || result="fail"
+
+	if [ "$result" = "pass" ]; then
+		ksft_test_result_pass \
+			"relay e2e: ping OK, cap=$cap_count " \
+			"passed=$total_passed discarded=$total_discarded"
+	else
+		ksft_test_result_fail \
+			"relay e2e: ping_rc=$ping_rc cap=$cap_count " \
+			"passed=$total_passed discarded=$total_discarded" \
+			"(expected ping OK, cap=$PING_COUNT," \
+			"passed>=$PING_COUNT, discarded>=$PING_COUNT)"
+	fi
+}
+
+# ----------------------------------------------------------------------------
+# Main
+# ----------------------------------------------------------------------------
+main()
+{
+	ksft_print_header
+	check_prerequisites
+	load_module
+	setup_topology
+
+	if ! check_frer_action; then
+		ksft_set_plan "$NUM_TESTS"
+		for i in $(seq 1 "$NUM_TESTS"); do
+			ksft_test_result_skip \
+				"frer action not available in this kernel (test $i)"
+		done
+		ksft_print_cnts
+		exit "$KSFT_SKIP"
+	fi
+
+	ksft_set_plan "$NUM_TESTS"
+
+	test_push_verify_bond        # TEST 1: push on a0/b0, no recover, R-TAG on both paths
+	test_shared_recover_bond     # TEST 2: shared recover, dedup, ping succeeds
+	test_individual_recover_bond # TEST 3: individual recover, no dedup, double frames
+	test_no_tag_pop_bond         # TEST 4: shared recover without tag-pop, R-TAG preserved
+	test_simple_point_to_point   # TEST 5: single-path p2p, no bond
+	test_relay_e2e               # TEST 6: relay bridge topology
+
+	ksft_print_cnts
+
+	[ "$_ksft_fail" -eq 0 ] && ksft_exit_pass || ksft_exit_fail
+}
+
+main "$@"
-- 
2.17.1


  parent reply	other threads:[~2026-06-22  9:20 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
2026-06-22  9:21 ` [PATCH net-next 1/6] uapi: if_ether: add ETH_P_RTAG for IEEE 802.1CB R-TAG Xiaoliang Yang
2026-06-22  9:21 ` [PATCH net-next 2/6] uapi: pkt_cls: add TCA_ID_FRER action identifier Xiaoliang Yang
2026-06-22  9:21 ` [PATCH net-next 3/6] uapi: tc_act: add tc_frer UAPI header Xiaoliang Yang
2026-06-22  9:21 ` [PATCH net-next 4/6] net: sched: act_frer: add FRER tc action Xiaoliang Yang
2026-06-22  9:21 ` [PATCH net-next 5/6] selftest: add tc-testing JSON test cases for act_frer Xiaoliang Yang
2026-06-22  9:21 ` Xiaoliang Yang [this message]
2026-06-22 15:59 ` [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Jakub Kicinski

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260622092118.6846-7-xiaoliang.yang_1@nxp.com \
    --to=xiaoliang.yang_1@nxp.com \
    --cc=davem@davemloft.net \
    --cc=edumazet@google.com \
    --cc=fejes@inf.elte.hu \
    --cc=horms@kernel.org \
    --cc=jhs@mojatatu.com \
    --cc=jiri@resnulli.us \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=shuah@kernel.org \
    --cc=vinicius.gomes@intel.com \
    --cc=vladimir.oltean@nxp.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox