All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB)
@ 2026-06-22  9:21 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
                   ` (6 more replies)
  0 siblings, 7 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

This series introduces a new TC action implementing
Frame Replication and Elimination for Reliability (FRER)
as defined in IEEE 802.1CB.

The FRER action enables:
- Frame replication (push)
- Sequence numbering via R-TAG
- Frame elimination based on sequence recovery

Patch overview:
 1. Add ETH_P_RTAG definition
 2. Introduce TCA_ID_FRER
 3. Add tc_frer uAPI
 4. Implement act_frer kernel module
 5. Add tc-testing selftest JSON coverage
 6. Add kselftest integration test

The implementation currently focuses on software datapath.  Hardware
offload is exposed through the flow offload API (FLOW_ACTION_FRER);
driver-side support for specific hardware will be submitted separately.

Usage scenarios:

=== Scenario 1a: Talker End - single port (no replication) ===

  The simplest case: a single egress path.  The frer push action
  inserts an R-TAG on the egress of the physical interface.  No
  mirror or virtual interface is needed.

    CPU
     |
    eth0 egress clsact:
         action frer push index 1  <- insert R-TAG seq=N
         |
        eth0
    [R-TAG seq=N | payload]
      Path A --> network

  Configuration:

    tc qdisc add dev eth0 clsact
    tc filter add dev eth0 egress protocol ip flower skip_hw \
        action frer push index 1

=== Scenario 1b: Talker End - dual port replication via bond + cross-mirror ===

  A bond interface (balance-rr) aggregates both physical ports.  The
  frer push action is placed on each slave's egress; each slave also
  mirrors every outgoing frame to the other slave.  This cross-mirror
  ensures that every frame transmitted by the bond (regardless of which
  slave the round-robin selects) carries an R-TAG and reaches both
  physical links.  If one link goes down, the bond continues on the
  remaining slave without any traffic interruption.

    CPU (socket on bond0)
         |
        bond0 (balance-rr)
        /          \
     eth0            eth1
    egress clsact:   egress clsact:
    action frer push index 1   action frer push index 1
    action mirred egress       action mirred egress
        mirror dev eth1            mirror dev eth0
         |                              |
        eth0                          eth1
    [R-TAG seq=N | payload]   [R-TAG seq=N | payload]
      Path A --> network         Path B --> network

  Configuration:

    ip link add bond0 type bond mode balance-rr miimon 100
    ip link set eth0 master bond0
    ip link set eth1 master bond0
    ip link set eth0 up
    ip link set eth1 up
    ip link set bond0 up
    ip addr add 192.0.2.1/24 dev bond0

    tc qdisc add dev eth0 clsact
    tc filter add dev eth0 egress protocol ip flower skip_hw \
        action frer push index 1 \
        action mirred egress mirror dev eth1

    tc qdisc add dev eth1 clsact
    tc filter add dev eth1 egress protocol ip flower skip_hw \
        action frer push index 1 \
        action mirred egress mirror dev eth0

=== Scenario 2: Listener End - shared sequence recovery via bond ===

  Both physical ports are bonded (balance-rr).  Each port's ingress
  references the same recover action by index.  The first copy of each
  sequence number passes (R-TAG stripped by tag-pop) and is delivered
  directly to the bond's IP stack; the duplicate is discarded.  No
  separate convergence interface is needed because the bond already
  provides a single IP address over both slaves.

    eth0 (Path A in)            eth1 (Path B in)
    [R-TAG seq=N | payload]     [R-TAG seq=N | payload]
          |                           |
    ingress clsact              ingress clsact
    flower: match stream        flower: match stream
    action frer recover   <-->  action frer recover
        index 10 (shared,           index 10 (shared,
        tag-pop, spinlock           same action object)
        protected)
          |                           |
          +-----------+---------------+
                      |
                   bond0 (IP_DST) ----> IP stack / CPU
                              [payload, R-TAG removed by tag-pop]

  Configuration:

    ip link add bond0 type bond mode balance-rr miimon 100
    ip link set eth0 master bond0
    ip link set eth1 master bond0
    ip link set eth0 up
    ip link set eth1 up
    ip link set bond0 up
    ip addr add 192.0.2.2/24 dev bond0

    tc qdisc add dev eth0 clsact
    tc filter add dev eth0 ingress protocol all flower skip_hw \
        action frer recover alg vector history-length 16 \
            reset-time 2000 tag-pop index 10

    tc qdisc add dev eth1 clsact
    tc filter add dev eth1 ingress protocol all flower skip_hw \
        action frer recover index 10

=== Scenario 3a: Relay System - ingress sequence recovery ===

  A relay node receives redundant streams on two ingress ports and
  eliminates duplicates before forwarding.  The two ingress ports
  share the same recover action by index.  The surviving frame is
  redirected to an egress port and forwarded to the next segment.

    upstream
      
    Path A --> swp0 (ingress)   Path B --> swp1 (ingress)
                 |                          |
           ingress clsact        ingress clsact
           flower: match stream  flower: match stream
           action frer recover   action frer recover
               index 10          index 10 (shared)
           action mirred         action mirred
               redirect              redirect
               dev swp2              dev swp2
                 |                     |
                 +----------+----------+
                            |
                         swp2 --> downstream

  Configuration:

    tc qdisc add dev swp0 clsact
    tc filter add dev swp0 ingress protocol all flower skip_hw \
        action frer recover alg vector history-length 16 \
            reset-time 2000 tag-pop index 10 \
        action mirred egress redirect dev swp2

    tc qdisc add dev swp1 clsact
    tc filter add dev swp1 ingress protocol all flower skip_hw \
        action frer recover index 10 \
        action mirred egress redirect dev swp2

=== Scenario 3b: Relay System - ingress frame replication (push) ===

  A relay node receives frames from a talker on swp0 ingress, inserts
  an R-TAG, and replicates them onto two egress ports towards the next
  network segment.  FDB learning and flooding are disabled on all relay
  ports; MAC forwarding entries are configured statically to prevent
  duplicate frames from looping through the bridge.

    upstream
         |
        swp0 ingress clsact:
         action frer push index 1         <- insert new R-TAG seq=M
         action mirred egress mirror dev swp2 <- copy to Path B'
         action mirred egress redirect dev swp1 <- to Path A'
         |                                      |
        swp1                                  swp2
    [R-TAG seq=M | payload]           [R-TAG seq=M | payload]
      Path A' --> downstream            Path B' --> downstream

  Configuration:

    tc qdisc add dev swp0 clsact
    tc filter add dev swp0 ingress protocol ip flower skip_hw \
        action frer push index 1 \
        action mirred egress mirror dev swp2 \
        action mirred egress redirect dev swp1

    # Disable FDB learning and flooding on all relay ports to prevent
    # duplicate frames from looping back through the bridge.
    bridge link set dev swp0 learning off flood off
    bridge link set dev swp1 learning off flood off
    bridge link set dev swp2 learning off flood off
    bridge fdb add DST_MAC dev swp1 master static
    bridge fdb add DST_MAC dev swp2 master static

Known limitations:

  1. Only R-TAG (EtherType 0xF1C1, IEEE 802.1CB Section 7.8) is
     currently supported as the redundancy tag type.  HSR
     (IEC 62439-3) and PRP (IEC 62439-3) tag formats are defined in
     the UAPI (TCA_FRER_TAG_HSR, TCA_FRER_TAG_PRP) but not yet
     implemented; attempts to use them are rejected with -EOPNOTSUPP.
     Support for HSR and PRP tags will be added in a follow-up series.

Changes since RFC (https://lkml.org/lkml/2021/9/28/535):

  1. The frer action can now be attached to either ingress or egress
     clsact.  For talker-end frame replication the action is placed on
     the egress of the outgoing interface. For relay-system replication
     the action is placed on the ingress of the receiving interface,
     followed by mirred redirect to the egress ports.

  2. Reset timer reworked following Vinicius Costa Gomes' review.

  3. Vector recovery algorithm corrected following Ferenc Fejes' review.

  4. A bond is used on the end system to aggregate two device interfaces.
     addressing Vladimir’s comment that TC-FRER is not applicable to end
     systems. See Scenario 1b(talker end) and Scenario 2(listener end).
     The kselftest script (frer_test.sh) test this on TEST 2.

  5. Added detailed usage scenario descriptions with ASCII topology
     diagrams.  Added tc-testing JSON test cases (32 cases) and a
     TAP-format kselftest script (frer_test.sh) with five end-to-end
     functional tests and one relay bridge topology test.

Xiaoliang Yang (6):
  uapi: if_ether: add ETH_P_RTAG for IEEE 802.1CB R-TAG
  uapi: pkt_cls: add TCA_ID_FRER action identifier
  uapi: tc_act: add tc_frer UAPI header
  net: sched: act_frer: add FRER tc action
  selftest: add tc-testing JSON test cases for act_frer
  selftests: net: add kselftest for IEEE 802.1CB FRER tc action

 include/net/flow_offload.h                    |   11 +
 include/net/tc_act/tc_frer.h                  |   71 ++
 include/uapi/linux/if_ether.h                 |    1 +
 include/uapi/linux/pkt_cls.h                  |    1 +
 include/uapi/linux/tc_act/tc_frer.h           |   89 ++
 net/sched/Kconfig                             |   16 +
 net/sched/Makefile                            |    1 +
 net/sched/act_frer.c                          |  835 ++++++++++++++
 tools/testing/selftests/net/Makefile          |    1 +
 tools/testing/selftests/net/frer_test.sh      | 1013 +++++++++++++++++
 .../tc-testing/tc-tests/actions/frer.json     |  785 +++++++++++++
 11 files changed, 2824 insertions(+)
 create mode 100644 include/net/tc_act/tc_frer.h
 create mode 100644 include/uapi/linux/tc_act/tc_frer.h
 create mode 100644 net/sched/act_frer.c
 create mode 100755 tools/testing/selftests/net/frer_test.sh
 create mode 100644 tools/testing/selftests/tc-testing/tc-tests/actions/frer.json

-- 
2.17.1


^ permalink raw reply	[flat|nested] 8+ messages in thread

* [PATCH net-next 1/6] uapi: if_ether: add ETH_P_RTAG for IEEE 802.1CB R-TAG
  2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
@ 2026-06-22  9:21 ` Xiaoliang Yang
  2026-06-22  9:21 ` [PATCH net-next 2/6] uapi: pkt_cls: add TCA_ID_FRER action identifier Xiaoliang Yang
                   ` (5 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

The IEEE 802.1CB-2017 standard defines the Redundancy Tag (R-TAG) with
EtherType 0xF1C1. Add ETH_P_RTAG to the kernel's EtherType definitions
so that it can be used by tc classifiers (e.g. cls_flower) and the FRER
tc action for stream identification on the ingress path.

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 include/uapi/linux/if_ether.h | 1 +
 1 file changed, 1 insertion(+)

diff --git a/include/uapi/linux/if_ether.h b/include/uapi/linux/if_ether.h
index fb5efc8e06cc..2d909078cde1 100644
--- a/include/uapi/linux/if_ether.h
+++ b/include/uapi/linux/if_ether.h
@@ -122,6 +122,7 @@
 #define ETH_P_DSA_8021Q	0xDADB		/* Fake VLAN Header for DSA [ NOT AN OFFICIALLY REGISTERED ID ] */
 #define ETH_P_DSA_A5PSW	0xE001		/* A5PSW Tag Value [ NOT AN OFFICIALLY REGISTERED ID ] */
 #define ETH_P_IFE	0xED3E		/* ForCES inter-FE LFB type */
+#define ETH_P_RTAG	0xF1C1		/* Redundancy Tag (IEEE 802.1CB) */
 #define ETH_P_AF_IUCV   0xFBFB		/* IBM af_iucv [ NOT AN OFFICIALLY REGISTERED ID ] */
 #define ETH_P_NXP_NETC  0xFD3A		/* NXP NETC DSA [ NOT AN OFFICIALLY REGISTERED ID ] */
 
-- 
2.17.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next 2/6] uapi: pkt_cls: add TCA_ID_FRER action identifier
  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 ` Xiaoliang Yang
  2026-06-22  9:21 ` [PATCH net-next 3/6] uapi: tc_act: add tc_frer UAPI header Xiaoliang Yang
                   ` (4 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

Register TCA_ID_FRER in the global tc action ID enum so that the FRER
tc action can be identified uniquely among all tc actions.

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 include/uapi/linux/pkt_cls.h | 1 +
 1 file changed, 1 insertion(+)

diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h
index 28d94b11d1aa..9b87f0455110 100644
--- a/include/uapi/linux/pkt_cls.h
+++ b/include/uapi/linux/pkt_cls.h
@@ -139,6 +139,7 @@ enum tca_id {
 	TCA_ID_MPLS,
 	TCA_ID_CT,
 	TCA_ID_GATE,
+	TCA_ID_FRER,
 	/* other actions go here */
 	__TCA_ID_MAX = 255
 };
-- 
2.17.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next 3/6] uapi: tc_act: add tc_frer UAPI header
  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 ` Xiaoliang Yang
  2026-06-22  9:21 ` [PATCH net-next 4/6] net: sched: act_frer: add FRER tc action Xiaoliang Yang
                   ` (3 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

Define the netlink attribute layout and enumerations for the FRER tc
action (IEEE 802.1CB Frame Replication and Elimination for Reliability).

The action is split into two functional sub-commands selected by the
TCA_FRER_FUNC attribute:

  TCA_FRER_FUNC_PUSH    - Egress: sequence number generation and R-TAG
                          insertion. The action inserts an R-TAG with
                          the current sequence number into the frame
                          before passing it on. When chained with
                          "action mirred egress mirror", the mirrored
                          copy already carries the R-TAG, so all
                          replicated frames on different egress paths
                          carry the same sequence number without any
                          additional shared state.

  TCA_FRER_FUNC_RECOVER - Ingress: duplicate detection and elimination.
                          Multiple ingress filters can share the same
                          recovery state by referencing the same action
                          index, implementing Sequence Recovery across
                          ports (IEEE 802.1CB Section 7.4.2).
                          When TCA_FRER_RCVY_INDIVIDUAL flag is set,
                          the action uses private per-action state
                          (Individual Recovery, Section 7.5).

Statistics attributes map directly to the managed objects defined in
IEEE 802.1CB Table 10-1.

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 include/uapi/linux/tc_act/tc_frer.h | 89 +++++++++++++++++++++++++++++
 1 file changed, 89 insertions(+)
 create mode 100644 include/uapi/linux/tc_act/tc_frer.h

diff --git a/include/uapi/linux/tc_act/tc_frer.h b/include/uapi/linux/tc_act/tc_frer.h
new file mode 100644
index 000000000000..241e90827e26
--- /dev/null
+++ b/include/uapi/linux/tc_act/tc_frer.h
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */
+/* Copyright 2026 NXP */
+
+#ifndef __LINUX_TC_FRER_H
+#define __LINUX_TC_FRER_H
+
+#include <linux/pkt_cls.h>
+
+/* Base parameters passed in TCA_FRER_PARMS */
+struct tc_frer {
+	tc_gen;
+};
+
+/**
+ * enum TCA_FRER_* - netlink attributes for the FRER tc action
+ *
+ * @TCA_FRER_FUNC:             Functional sub-command (tc_frer_func).
+ *                             Mandatory.
+ * @TCA_FRER_TAG_TYPE:         Redundancy tag type (tc_frer_tag_type).
+ *                             Mandatory.
+ *
+ * Push-specific attributes (TCA_FRER_FUNC_PUSH):
+ * Recover-specific attributes (TCA_FRER_FUNC_RECOVER):
+ * @TCA_FRER_RCVY_INDIVIDUAL:  Flag. Force Individual Recovery.
+ * @TCA_FRER_RCVY_ALG:         u8. Recovery algorithm (tc_frer_rcvy_alg).
+ * @TCA_FRER_RCVY_HISTORY_LEN: u8. SequenceHistory window size (1-32).
+ *                             Maps to frerSeqRcvyHistoryLength.
+ * @TCA_FRER_RCVY_RESET_MSEC:  u32. Reset timer in milliseconds.
+ *                             0 disables the timer.
+ *                             Maps to frerSeqRcvyResetMSec.
+ * @TCA_FRER_RCVY_TAKE_NO_SEQ: Flag. Accept frames without a redundancy
+ *                             tag and pass them unconditionally.
+ *                             Maps to frerSeqRcvyTakeNoSeq.
+ * @TCA_FRER_RCVY_TAG_POP:     Flag. Remove the redundancy tag from
+ *                             frames that pass the recovery function.
+ *
+ * Read-only statistics (filled on dump, IEEE 802.1CB Table 10-1):
+ * @TCA_FRER_STATS_TAGLESS_PKTS:       frerCpsSeqRcvyTaglessPackets
+ * @TCA_FRER_STATS_OUT_OF_ORDER_PKTS:  frerCpsSeqRcvyOutOfOrderPackets
+ * @TCA_FRER_STATS_ROGUE_PKTS:         frerCpsSeqRcvyRoguePackets
+ * @TCA_FRER_STATS_LOST_PKTS:          frerCpsSeqRcvyLostPackets
+ * @TCA_FRER_STATS_RESETS:             frerCpsSeqRcvyResets
+ * @TCA_FRER_STATS_PASSED_PKTS:        frerCpsSeqRcvyPassedPackets
+ * @TCA_FRER_STATS_DISCARDED_PKTS:     frerCpsSeqRcvyDiscardedPackets
+ * @TCA_FRER_STATS_SEQGEN_PKTS:        frerCpsSeqGenPackets
+ */
+enum {
+	TCA_FRER_UNSPEC,
+	TCA_FRER_TM,                       /* struct tcf_t */
+	TCA_FRER_PARMS,                    /* struct tc_frer */
+	TCA_FRER_PAD,
+	TCA_FRER_FUNC,                     /* u8: tc_frer_func */
+	TCA_FRER_TAG_TYPE,                 /* u8: tc_frer_tag_type */
+	TCA_FRER_RCVY_INDIVIDUAL,          /* NLA_FLAG */
+	TCA_FRER_RCVY_ALG,                 /* u8: tc_frer_rcvy_alg */
+	TCA_FRER_RCVY_HISTORY_LEN,         /* u8: 1-32 */
+	TCA_FRER_RCVY_RESET_MSEC,          /* u32 */
+	TCA_FRER_RCVY_TAKE_NO_SEQ,         /* NLA_FLAG */
+	TCA_FRER_RCVY_TAG_POP,             /* NLA_FLAG */
+	TCA_FRER_STATS_TAGLESS_PKTS,       /* u64 */
+	TCA_FRER_STATS_OUT_OF_ORDER_PKTS,  /* u64 */
+	TCA_FRER_STATS_ROGUE_PKTS,         /* u64 */
+	TCA_FRER_STATS_LOST_PKTS,          /* u64 */
+	TCA_FRER_STATS_RESETS,             /* u64 */
+	TCA_FRER_STATS_PASSED_PKTS,        /* u64 */
+	TCA_FRER_STATS_DISCARDED_PKTS,     /* u64 */
+	TCA_FRER_STATS_SEQGEN_PKTS,        /* u64 */
+	__TCA_FRER_MAX,
+};
+
+#define TCA_FRER_MAX (__TCA_FRER_MAX - 1)
+
+enum tc_frer_func {
+	TCA_FRER_FUNC_PUSH    = 1,
+	TCA_FRER_FUNC_RECOVER = 2,
+};
+
+enum tc_frer_tag_type {
+	TCA_FRER_TAG_RTAG = 1,
+	TCA_FRER_TAG_HSR,
+	TCA_FRER_TAG_PRP,
+};
+
+enum tc_frer_rcvy_alg {
+	TCA_FRER_RCVY_VECTOR_ALG = 0,  /* IEEE 802.1CB 7.4.3.4 */
+	TCA_FRER_RCVY_MATCH_ALG  = 1,  /* IEEE 802.1CB 7.4.3.5 */
+};
+
+#endif /* __LINUX_TC_FRER_H */
-- 
2.17.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next 4/6] net: sched: act_frer: add FRER tc action
  2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
                   ` (2 preceding siblings ...)
  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 ` Xiaoliang Yang
  2026-06-22  9:21 ` [PATCH net-next 5/6] selftest: add tc-testing JSON test cases for act_frer Xiaoliang Yang
                   ` (2 subsequent siblings)
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

Introduce the FRER tc action for IEEE 802.1CB.  This patch adds the
module skeleton, the shared sequence-generator infrastructure, the
TCA_FRER_FUNC_PUSH data path, and the TCA_FRER_FUNC_RECOVER data path.

Sequence generation (IEEE 802.1CB Section 7.4.1):
  Each push action embeds a struct frer_seqgen directly in tcf_frer,
  protected by a per-action spinlock.  The sequence counter wraps at
  65536 (16-bit R-TAG field).  When a Talker chains "action frer push"
  with "action mirred egress mirror", both the primary and the mirrored
  frame carry the same R-TAG because mirred copies the already-modified
  skb.  No changes to act_mirred are required (Split function,
  Section 7.7).

Sequence Recovery vs. Individual Recovery (IEEE 802.1CB Section 7.5):

  Sequence Recovery (cross-port deduplication):
    Multiple ingress filters on different ports share one recover
    action by referencing the same action index.  They all operate on
    the same struct frer_rcvy embedded in that tcf_frer instance and
    protected by a spinlock.  A frame arriving on any port is checked
    against the shared sequence history; the first copy passes and all
    later copies with the same sequence number are discarded.

  Individual Recovery (per-port independent deduplication):
    Each action uses its own frer_rcvy embedded directly in tcf_frer.
    Selected when the user sets the "individual" flag.

Recovery algorithms:
  Vector (7.4.3.4, default): 32-bit history bit-vector, handles
    out-of-order delivery within the window.
  Match (7.4.3.5): remembers only the last accepted sequence number.

Reset timer:
  An hrtimer fires after frerSeqRcvyResetMSec ms of inactivity.
  CLOCK_MONOTONIC is used throughout.  The reset runs in a workqueue
  to avoid holding the spinlock in the hrtimer callback.

R-TAG wire format (IEEE 802.1CB 7.8, EtherType 0xF1C1):
  [Dst MAC 6B][Src MAC 6B][Optional 802.1Q tag 4B][0xF1C1 2B]
  [Reserved 2B][Sequence Number 2B][Encapsulated EtherType 2B][Payload]

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 include/net/flow_offload.h   |  11 +
 include/net/tc_act/tc_frer.h |  71 +++
 net/sched/Kconfig            |  16 +
 net/sched/Makefile           |   1 +
 net/sched/act_frer.c         | 835 +++++++++++++++++++++++++++++++++++
 5 files changed, 934 insertions(+)
 create mode 100644 include/net/tc_act/tc_frer.h
 create mode 100644 net/sched/act_frer.c

diff --git a/include/net/flow_offload.h b/include/net/flow_offload.h
index 70a02ee14308..8d97a5f293e6 100644
--- a/include/net/flow_offload.h
+++ b/include/net/flow_offload.h
@@ -184,6 +184,7 @@ enum flow_action_id {
 	FLOW_ACTION_VLAN_PUSH_ETH,
 	FLOW_ACTION_VLAN_POP_ETH,
 	FLOW_ACTION_CONTINUE,
+	FLOW_ACTION_FRER,
 	NUM_FLOW_ACTIONS,
 };
 
@@ -329,6 +330,16 @@ struct flow_action_entry {
 		struct {				/* FLOW_ACTION_PPPOE_PUSH */
 			u16		sid;
 		} pppoe;
+		struct {                                /* FLOW_ACTION_FRER */
+			u8		func;
+			u8		tag_type;
+			bool		individual;
+			u8		rcvy_alg;
+			u8		rcvy_history_len;
+			u32		rcvy_reset_msec;
+			bool		tag_pop;
+			bool		take_no_seq;
+		} frer;
 	};
 	struct flow_action_cookie *user_cookie; /* user defined action cookie */
 };
diff --git a/include/net/tc_act/tc_frer.h b/include/net/tc_act/tc_frer.h
new file mode 100644
index 000000000000..5f6f8ca70813
--- /dev/null
+++ b/include/net/tc_act/tc_frer.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright 2026 NXP */
+
+#ifndef __NET_TC_FRER_H
+#define __NET_TC_FRER_H
+
+#include <net/act_api.h>
+#include <linux/tc_act/tc_frer.h>
+
+/**
+ * struct frer_seqgen - sequence number generator state (embedded in tcf_frer)
+ */
+struct frer_seqgen {
+	u32		gen_seq_num;
+	u64		seq_space;	/* 1 << 16 */
+	spinlock_t	lock;		/* protects frer_seqgen state */
+	u64		stats_pkts;	/* frerCpsSeqGenPackets */
+};
+
+/**
+ * struct frer_rcvy - sequence recovery state (embedded in tcf_frer)
+ *
+ */
+struct frer_rcvy {
+	u8		alg;
+	u8		history_len;	/* 1-32 */
+	u32		reset_msec;
+	u64		seq_space;
+	u32		rcvy_seq_num;
+	u32		seq_history;
+	bool		take_any;
+	bool		take_no_seq;
+	struct hrtimer	hrtimer;
+	spinlock_t	lock;		/* protects frer_rcvy state */
+	/* statistics */
+	u64		stats_tagless_pkts;
+	u64		stats_out_of_order_pkts;
+	u64		stats_rogue_pkts;
+	u64		stats_lost_pkts;
+	u64		stats_resets;
+	u64		stats_passed_pkts;
+	u64		stats_discarded_pkts;
+};
+
+/**
+ * struct tcf_frer - per tc_action FRER private data
+ */
+struct tcf_frer {
+	struct tc_action	common;
+	u8			func;
+	u8			tag_type;
+	bool			tag_pop;
+	bool			individual;	/* Individual Recovery flag */
+	/* push path */
+	struct frer_seqgen	seqgen;
+	/* recover path */
+	struct frer_rcvy	rcvy;
+};
+
+#define to_frer(a) ((struct tcf_frer *)(a))
+
+static inline bool is_tcf_frer(const struct tc_action *a)
+{
+#ifdef CONFIG_NET_CLS_ACT
+	if (a->ops && a->ops->id == TCA_ID_FRER)
+		return true;
+#endif
+	return false;
+}
+
+#endif /* __NET_TC_FRER_H */
diff --git a/net/sched/Kconfig b/net/sched/Kconfig
index 6ddff028b81a..7ca79b3eb5b3 100644
--- a/net/sched/Kconfig
+++ b/net/sched/Kconfig
@@ -939,6 +939,22 @@ config NET_ACT_GATE
 	  To compile this code as a module, choose M here: the
 	  module will be called act_gate.
 
+config NET_ACT_FRER
+	tristate "IEEE 802.1CB FRER tc action"
+	depends on NET_CLS_ACT
+	help
+	  Say Y here to enable the IEEE 802.1CB FRER tc action.  The action
+	  implements the Sequence Generation Function (egress R-TAG insertion
+	  with shared per-stream sequence counter) and the Sequence Recovery
+	  Function (ingress duplicate detection and elimination) described in
+	  IEEE 802.1CB-2017.
+
+	  Both Sequence Recovery (cross-port shared state via rcvy-id) and
+	  Individual Recovery (per-port independent state) are supported.
+
+	  To compile this code as a module, choose M here: the
+	  module will be called act_frer.
+
 config NET_IFE_SKBMARK
 	tristate "Support to encoding decoding skb mark on IFE action"
 	depends on NET_ACT_IFE
diff --git a/net/sched/Makefile b/net/sched/Makefile
index 5078ea84e6ad..d9f60434e7d7 100644
--- a/net/sched/Makefile
+++ b/net/sched/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_IFE_SKBTCINDEX)	+= act_meta_skbtcindex.o
 obj-$(CONFIG_NET_ACT_TUNNEL_KEY)+= act_tunnel_key.o
 obj-$(CONFIG_NET_ACT_CT)	+= act_ct.o
 obj-$(CONFIG_NET_ACT_GATE)	+= act_gate.o
+obj-$(CONFIG_NET_ACT_FRER)	+= act_frer.o
 obj-$(CONFIG_NET_SCH_FIFO)	+= sch_fifo.o
 obj-$(CONFIG_NET_SCH_HTB)	+= sch_htb.o
 obj-$(CONFIG_NET_SCH_HFSC)	+= sch_hfsc.o
diff --git a/net/sched/act_frer.c b/net/sched/act_frer.c
new file mode 100644
index 000000000000..7b6db643788d
--- /dev/null
+++ b/net/sched/act_frer.c
@@ -0,0 +1,835 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright 2026 NXP */
+
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/kernel.h>
+#include <linux/skbuff.h>
+#include <linux/rtnetlink.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/if_vlan.h>
+#include <linux/hrtimer.h>
+#include <linux/workqueue.h>
+#include <net/act_api.h>
+#include <net/netlink.h>
+#include <net/pkt_cls.h>
+#include <net/tc_act/tc_frer.h>
+
+/* ------------------------------------------------------------------ */
+/* R-TAG wire structures (IEEE 802.1CB 7.8)                          */
+/* ------------------------------------------------------------------ */
+
+struct r_tag {
+	__be16 reserved;
+	__be16 sequence_nr;
+	__be16 encap_proto;
+} __packed;
+
+static struct tc_action_ops act_frer_ops;
+
+/* ------------------------------------------------------------------ */
+/* Recovery reset machinery                                            */
+/* ------------------------------------------------------------------ */
+
+struct frer_rcvy_work {
+	struct work_struct	work;
+	struct frer_rcvy	*rcvy;
+};
+
+static void frer_rcvy_reset(struct frer_rcvy *rcvy)
+{
+	if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG) {
+		rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+		rcvy->seq_history  = 0;
+	}
+	rcvy->take_any = true;
+	rcvy->stats_resets++;
+}
+
+static void frer_rcvy_reset_work_fn(struct work_struct *work)
+{
+	struct frer_rcvy_work *rw =
+		container_of(work, struct frer_rcvy_work, work);
+	struct frer_rcvy *rcvy = rw->rcvy;
+
+	spin_lock_bh(&rcvy->lock);
+	frer_rcvy_reset(rcvy);
+	spin_unlock_bh(&rcvy->lock);
+	kfree(rw);
+}
+
+static enum hrtimer_restart frer_rcvy_hrtimer_fn(struct hrtimer *timer)
+{
+	struct frer_rcvy *rcvy =
+		container_of(timer, struct frer_rcvy, hrtimer);
+	struct frer_rcvy_work *rw;
+
+	/* Allocate in GFP_ATOMIC context; if it fails the state is not
+	 * reset this cycle - the next frame will attempt again.
+	 */
+	rw = kmalloc_obj(*rw);
+	if (rw) {
+		INIT_WORK(&rw->work, frer_rcvy_reset_work_fn);
+		rw->rcvy = rcvy;
+		schedule_work(&rw->work);
+	}
+	return HRTIMER_NORESTART;
+}
+
+static void frer_rcvy_timer_restart(struct frer_rcvy *rcvy)
+{
+	if (rcvy->reset_msec)
+		hrtimer_start(&rcvy->hrtimer,
+			      ms_to_ktime(rcvy->reset_msec),
+			      HRTIMER_MODE_REL_SOFT);
+}
+
+static void frer_rcvy_init_state(struct frer_rcvy *rcvy, u8 alg,
+				 u8 history_len, u32 reset_msec,
+				 bool take_no_seq)
+{
+	rcvy->alg          = alg;
+	rcvy->history_len  = history_len;
+	rcvy->reset_msec   = reset_msec;
+	rcvy->seq_space    = 1 << 16;
+	rcvy->take_no_seq  = take_no_seq;
+	rcvy->take_any     = true;
+	rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+	rcvy->seq_history  = 0;
+	spin_lock_init(&rcvy->lock);
+	hrtimer_setup(&rcvy->hrtimer, frer_rcvy_hrtimer_fn, CLOCK_MONOTONIC,
+		      HRTIMER_MODE_REL_SOFT);
+}
+
+/* ------------------------------------------------------------------ */
+/* R-TAG helpers                                                       */
+/* ------------------------------------------------------------------ */
+
+static int frer_rtag_push(struct sk_buff *skb, u16 seq_num)
+{
+	unsigned char *new_mac_header;
+	unsigned int data_offset;
+	unsigned int head_len;
+	struct vlan_ethhdr *vh;
+	struct ethhdr *eh;
+	struct r_tag *rtag;
+	__be16 *proto_ptr;
+	__be16 saved_proto;
+
+	if (!skb_mac_header_was_set(skb))
+		return -EINVAL;
+
+	data_offset = skb->data - skb_mac_header(skb);
+
+	if (skb_cow_head(skb, data_offset + sizeof(*rtag)))
+		return -ENOMEM;
+
+	if (data_offset > 0)
+		skb_push(skb, data_offset);
+
+	eh = eth_hdr(skb);
+	if (eth_type_vlan(eh->h_proto)) {
+		if (!pskb_may_pull(skb, sizeof(*vh)))
+			return -EINVAL;
+		eh = eth_hdr(skb);
+		vh = (struct vlan_ethhdr *)eh;
+		proto_ptr = &vh->h_vlan_encapsulated_proto;
+		head_len = sizeof(*vh);
+	} else {
+		if (!pskb_may_pull(skb, sizeof(*eh)))
+			return -EINVAL;
+		eh = eth_hdr(skb);
+		proto_ptr = &eh->h_proto;
+		head_len = sizeof(*eh);
+	}
+
+	saved_proto = *proto_ptr;
+	*proto_ptr = htons(ETH_P_RTAG);
+
+	skb_push(skb, sizeof(*rtag));
+	skb_reset_mac_header(skb);
+
+	new_mac_header = skb_mac_header(skb);
+	memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+	skb->protocol = htons(ETH_P_RTAG);
+	skb_set_network_header(skb, head_len);
+	if (data_offset > 0)
+		skb_pull(skb, data_offset);
+
+	/* Write R-TAG after the Ethernet / VLAN header */
+	rtag = (struct r_tag *)(new_mac_header + head_len);
+	rtag->reserved    = 0;
+	rtag->sequence_nr = htons(seq_num);
+	rtag->encap_proto = saved_proto;
+
+	return 0;
+}
+
+static void frer_rtag_pop(struct sk_buff *skb)
+{
+	unsigned char *new_mac_header;
+	unsigned int data_offset;
+	unsigned int head_len;
+	struct vlan_ethhdr *vh;
+	struct ethhdr *eh;
+	struct r_tag *rtag;
+	__be16 *proto_ptr;
+
+	data_offset = skb->data - skb_mac_header(skb);
+	if (data_offset > 0)
+		skb_push(skb, data_offset);
+
+	eh = eth_hdr(skb);
+	if (eth_type_vlan(eh->h_proto)) {
+		vh = (struct vlan_ethhdr *)eh;
+		proto_ptr = &vh->h_vlan_encapsulated_proto;
+		head_len = sizeof(*vh);
+	} else {
+		proto_ptr = &eh->h_proto;
+		head_len = sizeof(*eh);
+	}
+
+	if (*proto_ptr != htons(ETH_P_RTAG))
+		return;
+
+	rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+	*proto_ptr = rtag->encap_proto;
+
+	skb->protocol = rtag->encap_proto;
+
+	skb_postpull_rcsum(skb, rtag, sizeof(struct r_tag));
+	skb_pull(skb, sizeof(*rtag));
+	skb_reset_mac_header(skb);
+
+	new_mac_header = skb_mac_header(skb);
+	memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+	skb_set_network_header(skb, head_len);
+	if (data_offset > 0)
+		skb_pull(skb, data_offset);
+}
+
+static int frer_rtag_decode(struct sk_buff *skb, int *seq)
+{
+	unsigned int data_offset;
+	struct vlan_ethhdr *vh;
+	unsigned int head_len;
+	struct ethhdr *eh;
+	struct r_tag *rtag;
+	__be16 *proto_ptr;
+
+	if (!skb_mac_header_was_set(skb))
+		return -EINVAL;
+
+	data_offset = skb->data - skb_mac_header(skb);
+
+	if (skb_cow_head(skb, data_offset))
+		return -ENOMEM;
+
+	if (data_offset > 0)
+		skb_push(skb, data_offset);
+
+	eh = eth_hdr(skb);
+	if (eth_type_vlan(eh->h_proto)) {
+		if (!pskb_may_pull(skb, sizeof(*vh) + sizeof(*rtag)))
+			return -EINVAL;
+		eh = eth_hdr(skb);
+		vh = (struct vlan_ethhdr *)eh;
+		proto_ptr = &vh->h_vlan_encapsulated_proto;
+		head_len = sizeof(*vh);
+	} else {
+		if (!pskb_may_pull(skb, sizeof(*eh) + sizeof(*rtag)))
+			return -EINVAL;
+		eh = eth_hdr(skb);
+		proto_ptr = &eh->h_proto;
+		head_len = sizeof(*eh);
+	}
+
+	if (data_offset > 0)
+		skb_pull(skb, data_offset);
+
+	if (*proto_ptr != htons(ETH_P_RTAG)) {
+		*seq = -1;
+		return 0;
+	}
+
+	rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+
+	*seq = (int)ntohs(rtag->sequence_nr);
+
+	return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Recovery algorithms (called with rcvy->lock held)                  */
+/* ------------------------------------------------------------------ */
+
+/* Returns true = pass frame, false = discard frame.
+ * @individual: when true, restart the reset timer even on discarded frames
+ *   (rogue/duplicate), as required for Individual Recovery (IEEE 802.1CB 7.5).
+ */
+static bool frer_vector_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+	int delta;
+	bool restart_timer = false;
+	bool pass;
+
+	if (seq < 0) {
+		/* No R-TAG present */
+		rcvy->stats_tagless_pkts++;
+		if (rcvy->take_no_seq) {
+			restart_timer = true;
+			pass = true;
+		} else {
+			pass = false;
+		}
+		goto out;
+	}
+
+	if (rcvy->take_any) {
+		/* First frame after reset: accept unconditionally */
+		rcvy->take_any     = false;
+		rcvy->rcvy_seq_num = (u32)seq;
+		rcvy->seq_history  = BIT(0);
+		restart_timer = true;
+		pass = true;
+		goto out;
+	}
+
+	delta = (seq - (int)rcvy->rcvy_seq_num) &
+		(int)(rcvy->seq_space - 1);
+	/* Map delta > seq_space/2 to negative (signed wrap) */
+	if ((u32)delta & (u32)(rcvy->seq_space / 2))
+		delta -= (int)rcvy->seq_space;
+
+	if (delta >= (int)rcvy->history_len ||
+	    delta <= -(int)rcvy->history_len) {
+		/* Packet is out-of-range (rogue). */
+		rcvy->stats_rogue_pkts++;
+		if (individual)
+			restart_timer = true;
+		pass = false;
+		goto out;
+	}
+
+	if (delta <= 0) {
+		/* Packet is old: check whether already seen. */
+		if (rcvy->seq_history & BIT(-delta)) {
+			if (individual)
+				restart_timer = true;
+			/* Already received */
+			pass = false;
+		} else {
+			/* Out-of-order but not yet seen */
+			rcvy->seq_history |= BIT(-delta);
+			rcvy->stats_out_of_order_pkts++;
+			restart_timer = true;
+			pass = true;
+		}
+		goto out;
+	}
+
+	/* delta > 0: frame is newer than expected */
+	if (delta != 1)
+		rcvy->stats_out_of_order_pkts++;
+
+	/* Shift history forward, counting any gaps as lost */
+	while (--delta) {
+		if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+			rcvy->stats_lost_pkts++;
+		rcvy->seq_history <<= 1;
+	}
+	if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+		rcvy->stats_lost_pkts++;
+	rcvy->seq_history = (rcvy->seq_history << 1) | BIT(0);
+	rcvy->rcvy_seq_num = (u32)seq;
+	restart_timer = true;
+	pass = true;
+
+out:
+	if (restart_timer)
+		frer_rcvy_timer_restart(rcvy);
+	return pass;
+}
+
+static bool frer_match_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+	if (seq < 0) {
+		/* No R-TAG: Match alg cannot deduplicate, always pass. */
+		rcvy->stats_tagless_pkts++;
+		return true;
+	}
+
+	if (rcvy->take_any) {
+		rcvy->take_any     = false;
+		rcvy->rcvy_seq_num = (u32)seq;
+		frer_rcvy_timer_restart(rcvy);
+		return true;
+	}
+
+	if ((u32)seq == rcvy->rcvy_seq_num) {
+		/* Duplicate */
+		if (individual)
+			frer_rcvy_timer_restart(rcvy);
+		return false;
+	}
+
+	/* New sequence number: accept and update */
+	if ((u32)seq != ((rcvy->rcvy_seq_num + 1) % rcvy->seq_space))
+		rcvy->stats_out_of_order_pkts++;
+	rcvy->rcvy_seq_num = (u32)seq;
+	frer_rcvy_timer_restart(rcvy);
+	return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* Netlink policy                                                      */
+/* ------------------------------------------------------------------ */
+
+static const struct nla_policy frer_policy[TCA_FRER_MAX + 1] = {
+	[TCA_FRER_PARMS]            = NLA_POLICY_EXACT_LEN(sizeof(struct tc_frer)),
+	[TCA_FRER_FUNC]             = { .type = NLA_U8 },
+	[TCA_FRER_TAG_TYPE]         = { .type = NLA_U8 },
+	[TCA_FRER_RCVY_INDIVIDUAL]  = { .type = NLA_FLAG },
+	[TCA_FRER_RCVY_ALG]         = { .type = NLA_U8 },
+	[TCA_FRER_RCVY_HISTORY_LEN] = NLA_POLICY_RANGE(NLA_U8, 1, 32),
+	[TCA_FRER_RCVY_RESET_MSEC]  = { .type = NLA_U32 },
+	[TCA_FRER_RCVY_TAKE_NO_SEQ] = { .type = NLA_FLAG },
+	[TCA_FRER_RCVY_TAG_POP]     = { .type = NLA_FLAG },
+};
+
+/* ------------------------------------------------------------------ */
+/* Action init                                                         */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_init(struct net *net, struct nlattr *nla,
+			 struct nlattr *est, struct tc_action **a,
+			 struct tcf_proto *tp, u32 flags,
+			 struct netlink_ext_ack *extack)
+{
+	struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+	bool bind = flags & TCA_ACT_FLAGS_BIND;
+	struct nlattr *tb[TCA_FRER_MAX + 1];
+	struct tcf_chain *goto_ch = NULL;
+	struct tcf_frer *f;
+	struct tc_frer *parm;
+	bool exists = false;
+	int ret = 0, err, index;
+	u8 func, tag_type;
+
+	if (!nla) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: attributes required");
+		return -EINVAL;
+	}
+
+	err = nla_parse_nested(tb, TCA_FRER_MAX, nla, frer_policy, extack);
+	if (err < 0)
+		return err;
+
+	if (!tb[TCA_FRER_PARMS]) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_PARMS missing");
+		return -EINVAL;
+	}
+	if (!tb[TCA_FRER_FUNC]) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_FUNC missing");
+		return -EINVAL;
+	}
+	if (!tb[TCA_FRER_TAG_TYPE]) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_TAG_TYPE missing");
+		return -EINVAL;
+	}
+
+	func     = nla_get_u8(tb[TCA_FRER_FUNC]);
+	tag_type = nla_get_u8(tb[TCA_FRER_TAG_TYPE]);
+
+	if (func != TCA_FRER_FUNC_PUSH && func != TCA_FRER_FUNC_RECOVER) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: unknown func");
+		return -EINVAL;
+	}
+	if (tag_type != TCA_FRER_TAG_RTAG) {
+		NL_SET_ERR_MSG_MOD(extack, "frer: only rtag supported");
+		return -EOPNOTSUPP;
+	}
+
+	parm  = nla_data(tb[TCA_FRER_PARMS]);
+	index = parm->index;
+
+	err = tcf_idr_check_alloc(tn, &index, a, bind);
+	if (err < 0)
+		return err;
+	exists = err;
+
+	if (exists && bind)
+		return ACT_P_BOUND;
+
+	if (!exists) {
+		ret = tcf_idr_create_from_flags(tn, index, est, a,
+						&act_frer_ops, bind, flags);
+		if (ret) {
+			tcf_idr_cleanup(tn, index);
+			return ret;
+		}
+		ret = ACT_P_CREATED;
+	} else if (!(flags & TCA_ACT_FLAGS_REPLACE)) {
+		tcf_idr_release(*a, bind);
+		return -EEXIST;
+	}
+
+	err = tcf_action_check_ctrlact(parm->action, tp, &goto_ch, extack);
+	if (err < 0)
+		goto release_idr;
+
+	f = to_frer(*a);
+
+	spin_lock_bh(&f->tcf_lock);
+	goto_ch = tcf_action_set_ctrlact(*a, parm->action, goto_ch);
+	f->func     = func;
+	f->tag_type = tag_type;
+	f->tag_pop  = !!tb[TCA_FRER_RCVY_TAG_POP];
+
+	if (func == TCA_FRER_FUNC_PUSH) {
+		if (ret == ACT_P_CREATED) {
+			spin_lock_init(&f->seqgen.lock);
+			f->seqgen.seq_space = 1 << 16;
+		}
+		/* gen_seq_num starts at 0 on creation; preserved on replace */
+	} else {
+		u8 alg = tb[TCA_FRER_RCVY_ALG] ?
+			 nla_get_u8(tb[TCA_FRER_RCVY_ALG]) :
+			 TCA_FRER_RCVY_VECTOR_ALG;
+		u8 history_len = tb[TCA_FRER_RCVY_HISTORY_LEN] ?
+				 nla_get_u8(tb[TCA_FRER_RCVY_HISTORY_LEN]) : 32;
+		u32 reset_msec = tb[TCA_FRER_RCVY_RESET_MSEC] ?
+				 nla_get_u32(tb[TCA_FRER_RCVY_RESET_MSEC]) : 0;
+		bool take_no_seq = !!tb[TCA_FRER_RCVY_TAKE_NO_SEQ];
+
+		if (alg != TCA_FRER_RCVY_VECTOR_ALG &&
+		    alg != TCA_FRER_RCVY_MATCH_ALG) {
+			spin_unlock_bh(&f->tcf_lock);
+			NL_SET_ERR_MSG_MOD(extack, "frer: unknown recovery algorithm");
+			err = -EINVAL;
+			goto release_idr;
+		}
+
+		f->individual = !!tb[TCA_FRER_RCVY_INDIVIDUAL];
+
+		/* Cancel any running reset timer before re-initialising. */
+		if (ret != ACT_P_CREATED && f->rcvy.reset_msec) {
+			spin_unlock_bh(&f->tcf_lock);
+			hrtimer_cancel(&f->rcvy.hrtimer);
+			spin_lock_bh(&f->tcf_lock);
+		}
+
+		frer_rcvy_init_state(&f->rcvy, alg, history_len,
+				     reset_msec, take_no_seq);
+	}
+
+	spin_unlock_bh(&f->tcf_lock);
+
+	if (goto_ch)
+		tcf_chain_put_by_act(goto_ch);
+
+	return ret;
+
+release_idr:
+	tcf_idr_release(*a, bind);
+	return err;
+}
+
+/* ------------------------------------------------------------------ */
+/* Data path                                                           */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_act(struct sk_buff *skb, const struct tc_action *a,
+			struct tcf_result *res)
+{
+	struct tcf_frer *f = to_frer(a);
+	int retval;
+
+	tcf_lastuse_update(&f->tcf_tm);
+	tcf_action_update_bstats(&f->common, skb);
+	retval = READ_ONCE(f->tcf_action);
+
+	if (f->func == TCA_FRER_FUNC_PUSH) {
+		struct frer_seqgen *sg = &f->seqgen;
+		u16 seq;
+
+		spin_lock(&sg->lock);
+		seq = (u16)sg->gen_seq_num;
+		if (++sg->gen_seq_num >= sg->seq_space)
+			sg->gen_seq_num = 0;
+		sg->stats_pkts++;
+		spin_unlock(&sg->lock);
+
+		if (frer_rtag_push(skb, seq) < 0) {
+			tcf_action_inc_drop_qstats(&f->common);
+			return TC_ACT_SHOT;
+		}
+	} else {
+		struct frer_rcvy *rcvy = &f->rcvy;
+		bool pass;
+		int seq;
+
+		if (frer_rtag_decode(skb, &seq) < 0) {
+			tcf_action_inc_drop_qstats(&f->common);
+			return TC_ACT_SHOT;
+		}
+
+		spin_lock(&rcvy->lock);
+		if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG)
+			pass = frer_vector_alg(rcvy, seq, f->individual);
+		else
+			pass = frer_match_alg(rcvy, seq, f->individual);
+
+		if (pass) {
+			rcvy->stats_passed_pkts++;
+			spin_unlock(&rcvy->lock);
+			if (f->tag_pop)
+				frer_rtag_pop(skb);
+			return retval;
+		}
+
+		rcvy->stats_discarded_pkts++;
+		spin_unlock(&rcvy->lock);
+		return TC_ACT_SHOT;
+	}
+
+	return retval;
+}
+
+/* ------------------------------------------------------------------ */
+/* Dump                                                                */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_dump(struct sk_buff *skb, struct tc_action *a,
+			 int bind, int ref)
+{
+	unsigned char *b = skb_tail_pointer(skb);
+	struct tcf_frer *f = to_frer(a);
+	struct tc_frer opt = {
+		.index   = f->tcf_index,
+		.refcnt  = refcount_read(&f->tcf_refcnt) - ref,
+		.bindcnt = atomic_read(&f->tcf_bindcnt) - bind,
+	};
+	struct tcf_t t;
+
+	spin_lock_bh(&f->tcf_lock);
+	opt.action = f->tcf_action;
+
+	if (nla_put(skb, TCA_FRER_PARMS, sizeof(opt), &opt))
+		goto nla_put_failure;
+	if (nla_put_u8(skb, TCA_FRER_FUNC, f->func))
+		goto nla_put_failure;
+	if (nla_put_u8(skb, TCA_FRER_TAG_TYPE, f->tag_type))
+		goto nla_put_failure;
+	if (f->tag_pop && nla_put_flag(skb, TCA_FRER_RCVY_TAG_POP))
+		goto nla_put_failure;
+
+	if (f->func == TCA_FRER_FUNC_PUSH) {
+		spin_lock(&f->seqgen.lock);
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_SEQGEN_PKTS,
+				      f->seqgen.stats_pkts, TCA_FRER_PAD)) {
+			spin_unlock(&f->seqgen.lock);
+			goto nla_put_failure;
+		}
+		spin_unlock(&f->seqgen.lock);
+	} else {
+		u64 tagless, ooo, rogue, lost, resets, passed, discarded;
+		struct frer_rcvy *rcvy = &f->rcvy;
+
+		spin_lock(&rcvy->lock);
+		tagless    = rcvy->stats_tagless_pkts;
+		ooo        = rcvy->stats_out_of_order_pkts;
+		rogue      = rcvy->stats_rogue_pkts;
+		lost       = rcvy->stats_lost_pkts;
+		resets     = rcvy->stats_resets;
+		passed     = rcvy->stats_passed_pkts;
+		discarded  = rcvy->stats_discarded_pkts;
+		spin_unlock(&rcvy->lock);
+
+		if (f->individual && nla_put_flag(skb, TCA_FRER_RCVY_INDIVIDUAL))
+			goto nla_put_failure;
+		if (nla_put_u8(skb, TCA_FRER_RCVY_ALG, rcvy->alg))
+			goto nla_put_failure;
+		if (nla_put_u8(skb, TCA_FRER_RCVY_HISTORY_LEN, rcvy->history_len))
+			goto nla_put_failure;
+		if (nla_put_u32(skb, TCA_FRER_RCVY_RESET_MSEC, rcvy->reset_msec))
+			goto nla_put_failure;
+		if (rcvy->take_no_seq && nla_put_flag(skb, TCA_FRER_RCVY_TAKE_NO_SEQ))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_TAGLESS_PKTS,
+				      tagless, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_OUT_OF_ORDER_PKTS,
+				      ooo, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_ROGUE_PKTS,
+				      rogue, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_LOST_PKTS,
+				      lost, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_RESETS,
+				      resets, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_PASSED_PKTS,
+				      passed, TCA_FRER_PAD))
+			goto nla_put_failure;
+		if (nla_put_u64_64bit(skb, TCA_FRER_STATS_DISCARDED_PKTS,
+				      discarded, TCA_FRER_PAD))
+			goto nla_put_failure;
+	}
+
+	tcf_tm_dump(&t, &f->tcf_tm);
+	if (nla_put_64bit(skb, TCA_FRER_TM, sizeof(t), &t, TCA_FRER_PAD))
+		goto nla_put_failure;
+
+	spin_unlock_bh(&f->tcf_lock);
+	return skb->len;
+
+nla_put_failure:
+	spin_unlock_bh(&f->tcf_lock);
+	nlmsg_trim(skb, b);
+	return -1;
+}
+
+/* ------------------------------------------------------------------ */
+/* Cleanup                                                             */
+/* ------------------------------------------------------------------ */
+
+static void tcf_frer_cleanup(struct tc_action *a)
+{
+	struct tcf_frer *f = to_frer(a);
+
+	if (f->func == TCA_FRER_FUNC_RECOVER)
+		hrtimer_cancel(&f->rcvy.hrtimer);
+}
+
+/* ------------------------------------------------------------------ */
+/* Walker / search / stats / fill-size / offload                      */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_walker(struct net *net, struct sk_buff *skb,
+			   struct netlink_callback *cb, int type,
+			   const struct tc_action_ops *ops,
+			   struct netlink_ext_ack *extack)
+{
+	struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+	return tcf_generic_walker(tn, skb, cb, type, ops, extack);
+}
+
+static void tcf_frer_stats_update(struct tc_action *a, u64 bytes, u64 packets,
+				  u64 drops, u64 lastuse, bool hw)
+{
+	struct tcf_frer *f = to_frer(a);
+	struct tcf_t *tm = &f->tcf_tm;
+
+	tcf_action_update_stats(a, bytes, packets, drops, hw);
+	tm->lastuse = max_t(u64, tm->lastuse, lastuse);
+}
+
+static size_t tcf_frer_get_fill_size(const struct tc_action *act)
+{
+	return nla_total_size(sizeof(struct tc_frer)) /* TCA_FRER_PARMS */
+		+ nla_total_size(sizeof(u8)) /* TCA_FRER_FUNC */
+		+ nla_total_size(sizeof(u8)) /* TCA_FRER_TAG_TYPE */
+		+ nla_total_size(0) /* TCA_FRER_RCVY_TAG_POP (flag) */
+		+ nla_total_size(0) /* TCA_FRER_RCVY_INDIVIDUAL (flag) */
+		+ nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_ALG */
+		+ nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_HISTORY_LEN */
+		+ nla_total_size(sizeof(u32)) /* TCA_FRER_RCVY_RESET_MSEC */
+		+ nla_total_size(0) /* TCA_FRER_RCVY_TAKE_NO_SEQ (flag) */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_TAGLESS_PKTS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_OUT_OF_ORDER_PKTS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_ROGUE_PKTS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_LOST_PKTS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_RESETS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_PASSED_PKTS */
+		+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_DISCARDED_PKTS */
+		+ nla_total_size_64bit(sizeof(struct tcf_t)); /* TCA_FRER_TM */
+}
+
+static int tcf_frer_offload_act_setup(struct tc_action *act, void *entry_data,
+				      u32 *index_inc, bool bind,
+				      struct netlink_ext_ack *extack)
+{
+	if (bind) {
+		struct flow_action_entry *entry = entry_data;
+		struct tcf_frer *f = to_frer(act);
+
+		entry->id            = FLOW_ACTION_FRER;
+		entry->frer.func     = f->func;
+		entry->frer.tag_type = f->tag_type;
+		entry->frer.tag_pop  = f->tag_pop;
+
+		if (f->func != TCA_FRER_FUNC_PUSH) {
+			entry->frer.individual       = f->individual;
+			entry->frer.rcvy_alg         = f->rcvy.alg;
+			entry->frer.rcvy_history_len = f->rcvy.history_len;
+			entry->frer.rcvy_reset_msec  = f->rcvy.reset_msec;
+			entry->frer.take_no_seq      = f->rcvy.take_no_seq;
+		}
+		*index_inc = 1;
+	} else {
+		struct flow_offload_action *fl_action = entry_data;
+
+		fl_action->id = FLOW_ACTION_FRER;
+	}
+	return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Module glue                                                         */
+/* ------------------------------------------------------------------ */
+
+static struct tc_action_ops act_frer_ops = {
+	.kind		    = "frer",
+	.id		    = TCA_ID_FRER,
+	.owner		    = THIS_MODULE,
+	.act		    = tcf_frer_act,
+	.init		    = tcf_frer_init,
+	.cleanup	    = tcf_frer_cleanup,
+	.dump		    = tcf_frer_dump,
+	.walk		    = tcf_frer_walker,
+	.stats_update	    = tcf_frer_stats_update,
+	.get_fill_size	    = tcf_frer_get_fill_size,
+	.offload_act_setup  = tcf_frer_offload_act_setup,
+	.size		    = sizeof(struct tcf_frer),
+};
+
+static __net_init int frer_init_net(struct net *net)
+{
+	struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+	return tc_action_net_init(net, tn, &act_frer_ops);
+}
+
+static void __net_exit frer_exit_net(struct list_head *net_list)
+{
+	tc_action_net_exit(net_list, act_frer_ops.net_id);
+}
+
+static struct pernet_operations frer_net_ops = {
+	.init       = frer_init_net,
+	.exit_batch = frer_exit_net,
+	.id         = &act_frer_ops.net_id,
+	.size       = sizeof(struct tc_action_net),
+};
+
+static int __init frer_init_module(void)
+{
+	return tcf_register_action(&act_frer_ops, &frer_net_ops);
+}
+
+static void __exit frer_cleanup_module(void)
+{
+	tcf_unregister_action(&act_frer_ops, &frer_net_ops);
+}
+
+module_init(frer_init_module);
+module_exit(frer_cleanup_module);
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("IEEE 802.1CB FRER tc action");
-- 
2.17.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next 5/6] selftest: add tc-testing JSON test cases for act_frer
  2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
                   ` (3 preceding siblings ...)
  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 ` Xiaoliang Yang
  2026-06-22  9:21 ` [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action Xiaoliang Yang
  2026-06-22 15:59 ` [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Jakub Kicinski
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

Add a tc-testing JSON file covering the FRER (IEEE 802.1CB Frame
Replication and Elimination for Reliability) tc action (act_frer).

The test suite contains 32 test cases and exercises:

 - Creating push and recover actions with default and explicit parameters
   (tag-type, alg vector/match, history-length, reset-time, tag-pop,
   individual, take-no-seq)
 - Boundary values for history-length (1 and 32) and reset-time (0)
 - Combining multiple flags (frer_0011, frer_0012)
 - Statistics output format for push (SeqGen) and recover (passed,
   discarded, tagless, out-of-order, rogue, lost, resets)
 - Replace and delete operations
 - Flush all actions
 - Duplicate-index failure (expExitCode 255)
 - Control actions (continue, pipe) placed after the index token
 - Binding push and recover actions to egress/ingress clsact filters
 - Sharing a recover action across two filters and verifying the
   reference count increments
 - not_in_hw flag present in show output

Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
 .../tc-testing/tc-tests/actions/frer.json     | 785 ++++++++++++++++++
 1 file changed, 785 insertions(+)
 create mode 100644 tools/testing/selftests/tc-testing/tc-tests/actions/frer.json

diff --git a/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json
new file mode 100644
index 000000000000..d5be6ae156f7
--- /dev/null
+++ b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json
@@ -0,0 +1,785 @@
+[
+  {
+    "id": "frer_0001",
+    "name": "Create frer push action with default parameters",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer push index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 1",
+    "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0002",
+    "name": "Create frer push action with explicit tag-type rtag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer push tag-type rtag index 2",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 2",
+    "matchPattern": "action order [0-9]+: frer push tag-type rtag index 2",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0003",
+    "name": "Create frer recover action with default parameters",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 10",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10 alg vector history-length [0-9]+ reset-time [0-9]+",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0004",
+    "name": "Create frer recover action with vector algorithm explicit",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector index 11",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 11",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 11 alg vector",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0005",
+    "name": "Create frer recover action with match algorithm",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg match index 12",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 12",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 12 alg match",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0006",
+    "name": "Create frer recover action with history-length 16",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 16 index 13",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 13",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 13 alg vector history-length 16",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0007",
+    "name": "Create frer recover action with reset-time 2000",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector reset-time 2000 index 14",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 14",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 14 alg vector history-length [0-9]+ reset-time 2000",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0008",
+    "name": "Create frer recover action with tag-pop flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover tag-pop index 15",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 15",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 15.*tag-pop",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0009",
+    "name": "Create frer recover action with individual flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover individual index 16",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 16",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 16.*individual",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0010",
+    "name": "Create frer recover action with take-no-seq flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover take-no-seq index 17",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 17",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 17.*take-no-seq",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0011",
+    "name": "Create frer recover action with all parameters combined",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop individual index 20",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 20",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 20 individual alg vector history-length 16 reset-time 1000 tag-pop",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0012",
+    "name": "Create frer recover action with match alg and all flags",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg match take-no-seq tag-pop individual index 21",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 21",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 21 individual alg match history-length [0-9]+ reset-time [0-9]+ tag-pop take-no-seq",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0013",
+    "name": "Show frer push action SeqGen statistics (zero after create)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC -s actions show action frer index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC -s actions show action frer index 1",
+    "matchPattern": "SeqGen packets: 0",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0014",
+    "name": "Show frer recover action Statistics line (zero after create)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop index 10"
+    ],
+    "cmdUnderTest": "$TC -s actions show action frer index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC -s actions show action frer index 10",
+    "matchPattern": "Statistics: passed=0 discarded=0 tagless=0 out-of-order=0 rogue=0 lost=0 resets=0",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0015",
+    "name": "Show frer recover action Statistics fields present",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer recover index 10"
+    ],
+    "cmdUnderTest": "$TC -s actions show action frer index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC -s actions show action frer index 10",
+    "matchPattern": "Statistics: passed=[0-9]+ discarded=[0-9]+ tagless=[0-9]+ out-of-order=[0-9]+ rogue=[0-9]+ lost=[0-9]+ resets=[0-9]+",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0016",
+    "name": "Replace frer push action (same index)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC actions replace action frer push index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 1",
+    "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0017",
+    "name": "Replace frer recover action changing algorithm from vector to match",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer recover alg vector index 10"
+    ],
+    "cmdUnderTest": "$TC actions replace action frer recover alg match index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 10",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10 alg match",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0018",
+    "name": "Delete frer push action by index",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC actions del action frer index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer",
+    "matchPattern": "frer push tag-type rtag index 1",
+    "matchCount": "0",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0019",
+    "name": "Flush all frer actions",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1",
+      "$TC actions add action frer recover index 10",
+      "$TC actions add action frer recover index 11"
+    ],
+    "cmdUnderTest": "$TC actions flush action frer",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer",
+    "matchPattern": "action order [0-9]+: frer",
+    "matchCount": "0",
+    "teardown": [
+      "$TC actions flush action frer 2>/dev/null || true"
+    ]
+  },
+  {
+    "id": "frer_0020",
+    "name": "Add duplicate frer action index fails without replace flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC actions add action frer push index 1",
+    "expExitCode": "255",
+    "verifyCmd": "$TC actions show action frer index 1",
+    "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0021",
+    "name": "Create frer push action with continue control action",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer push index 1 continue",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 1",
+    "matchPattern": "action order [0-9]+: frer push tag-type rtag index 1.*control continue",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0022",
+    "name": "Create frer recover action with pipe control action",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover index 10 pipe",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 10",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 10.*control pipe",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0023",
+    "name": "Create frer recover action history-length minimum boundary (1)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 1 index 30",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 30",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 30 alg vector history-length 1",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0024",
+    "name": "Create frer recover action history-length maximum boundary (32)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector history-length 32 index 31",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 31",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 31 alg vector history-length 32",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0025",
+    "name": "Create frer recover action with reset-time 0 (timer disabled)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ]
+    ],
+    "cmdUnderTest": "$TC actions add action frer recover alg vector reset-time 0 index 32",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 32",
+    "matchPattern": "action order [0-9]+: frer recover tag-type rtag index 32 alg vector history-length [0-9]+ reset-time 0",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0026",
+    "name": "List all frer actions shows correct count",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1",
+      "$TC actions add action frer recover alg vector index 10",
+      "$TC actions add action frer recover alg match tag-pop index 11"
+    ],
+    "cmdUnderTest": "$TC actions show action frer",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer",
+    "matchPattern": "action order [0-9]+: frer",
+    "matchCount": "3",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0027",
+    "name": "Bind frer push action to egress clsact filter",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "ip link del frer_dummy 2>/dev/null || true",
+      "ip link add frer_dummy type dummy",
+      "ip link set frer_dummy up",
+      "$TC qdisc add dev frer_dummy clsact"
+    ],
+    "cmdUnderTest": "$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC filter show dev frer_dummy egress",
+    "matchPattern": "frer push tag-type rtag index 1",
+    "matchCount": "1",
+    "teardown": [
+      "$TC qdisc del dev frer_dummy clsact",
+      "$TC actions flush action frer",
+      "ip link del frer_dummy"
+    ]
+  },
+  {
+    "id": "frer_0028",
+    "name": "Bind frer recover action to ingress clsact filter",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "ip link del frer_dummy 2>/dev/null || true",
+      "ip link add frer_dummy type dummy",
+      "ip link set frer_dummy up",
+      "$TC qdisc add dev frer_dummy clsact"
+    ],
+    "cmdUnderTest": "$TC filter add dev frer_dummy ingress protocol all flower skip_hw action frer recover alg vector history-length 16 reset-time 1000 tag-pop index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC filter show dev frer_dummy ingress",
+    "matchPattern": "frer recover tag-type rtag index 10 alg vector history-length 16 reset-time 1000",
+    "matchCount": "1",
+    "teardown": [
+      "$TC qdisc del dev frer_dummy clsact",
+      "$TC actions flush action frer",
+      "ip link del frer_dummy"
+    ]
+  },
+  {
+    "id": "frer_0029",
+    "name": "Share frer recover action across two ingress filters (refcount check)",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "ip link del frer_a 2>/dev/null || true",
+      "ip link del frer_b 2>/dev/null || true",
+      "ip link add frer_a type dummy",
+      "ip link add frer_b type dummy",
+      "ip link set frer_a up",
+      "ip link set frer_b up",
+      "$TC qdisc add dev frer_a clsact",
+      "$TC qdisc add dev frer_b clsact",
+      "$TC filter add dev frer_a ingress protocol all flower skip_hw action frer recover alg vector history-length 16 tag-pop index 10"
+    ],
+    "cmdUnderTest": "$TC filter add dev frer_b ingress protocol all flower skip_hw action frer recover index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC -s actions show action frer index 10",
+    "matchPattern": "ref [2-9][0-9]*",
+    "matchCount": "1",
+    "teardown": [
+      "$TC qdisc del dev frer_a clsact",
+      "$TC qdisc del dev frer_b clsact",
+      "$TC actions flush action frer",
+      "ip link del frer_a",
+      "ip link del frer_b"
+    ]
+  },
+  {
+    "id": "frer_0030",
+    "name": "frer push action refcount increments when bound to filter",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "ip link del frer_dummy 2>/dev/null || true",
+      "ip link add frer_dummy type dummy",
+      "ip link set frer_dummy up",
+      "$TC qdisc add dev frer_dummy clsact",
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC -s actions show action frer index 1",
+    "matchPattern": "ref [2-9][0-9]*",
+    "matchCount": "1",
+    "teardown": [
+      "$TC qdisc del dev frer_dummy clsact",
+      "$TC actions flush action frer",
+      "ip link del frer_dummy"
+    ]
+  },
+  {
+    "id": "frer_0031",
+    "name": "frer push output shows not_in_hw flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer push index 1"
+    ],
+    "cmdUnderTest": "$TC actions show action frer index 1",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 1",
+    "matchPattern": "not_in_hw",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  },
+  {
+    "id": "frer_0032",
+    "name": "frer recover output shows not_in_hw flag",
+    "category": [
+      "actions",
+      "frer"
+    ],
+    "setup": [
+      [
+        "modprobe act_frer",
+        0,
+        1
+      ],
+      "$TC actions add action frer recover index 10"
+    ],
+    "cmdUnderTest": "$TC actions show action frer index 10",
+    "expExitCode": "0",
+    "verifyCmd": "$TC actions show action frer index 10",
+    "matchPattern": "not_in_hw",
+    "matchCount": "1",
+    "teardown": [
+      "$TC actions flush action frer"
+    ]
+  }
+]
-- 
2.17.1


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action
  2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
                   ` (4 preceding siblings ...)
  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
  2026-06-22 15:59 ` [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Jakub Kicinski
  6 siblings, 0 replies; 8+ messages in thread
From: Xiaoliang Yang @ 2026-06-22  9:21 UTC (permalink / raw)
  To: netdev, linux-kernel, linux-kselftest
  Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, shuah,
	vladimir.oltean, vinicius.gomes, fejes, xiaoliang.yang_1

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


^ permalink raw reply related	[flat|nested] 8+ messages in thread

* Re: [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB)
  2026-06-22  9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
                   ` (5 preceding siblings ...)
  2026-06-22  9:21 ` [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action Xiaoliang Yang
@ 2026-06-22 15:59 ` Jakub Kicinski
  6 siblings, 0 replies; 8+ messages in thread
From: Jakub Kicinski @ 2026-06-22 15:59 UTC (permalink / raw)
  To: Xiaoliang Yang
  Cc: netdev, linux-kernel, linux-kselftest, davem, edumazet, pabeni,
	jhs, jiri, horms, shuah, vladimir.oltean, vinicius.gomes, fejes

On Mon, 22 Jun 2026 17:21:12 +0800 Xiaoliang Yang wrote:
> This series introduces a new TC action implementing
> Frame Replication and Elimination for Reliability (FRER)
> as defined in IEEE 802.1CB.

## Form letter - net-next-closed

We have already submitted our pull request with net-next material for v7.2,
and therefore net-next is closed for new drivers, features, code refactoring
and optimizations. We are currently accepting bug fixes only.

Please repost when net-next reopens after June 29th.

RFC patches sent for review only are obviously welcome at any time.

See: https://www.kernel.org/doc/html/next/process/maintainer-netdev.html#development-cycle
-- 
pw-bot: defer
pv-bot: closed

^ permalink raw reply	[flat|nested] 8+ messages in thread

end of thread, other threads:[~2026-06-22 15:59 UTC | newest]

Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action Xiaoliang Yang
2026-06-22 15:59 ` [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Jakub Kicinski

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.