| Seq | Timestamp | Type | Name | Duration |
|---|---|---|---|---|
| 0/0 | 2026/06/22 09:40 | flow | patch-triage |
0mArgs:null Results: {
"EnableConfigs": [
"NET_ACT_FRER"
],
"FocusSymbols": [
"tcf_frer_init",
"tcf_frer_act",
"tcf_frer_dump",
"tcf_frer_cleanup",
"frer_rtag_push",
"frer_rtag_pop",
"frer_rtag_decode",
"frer_vector_alg",
"frer_match_alg",
"frer_rcvy_hrtimer_fn",
"frer_rcvy_reset_work_fn"
],
"Reasoning": "The patch introduces a new traffic control action `act_frer` for IEEE 802.1CB FRER. It includes packet manipulation (push/pop of R-TAG) and sequence recovery algorithms. This is reachable core kernel logic and should be fuzzed.",
"WorthFuzzing": true
} |
| 1/1 | 2026/06/22 09:40 | action | read-patch-diff |
0mArgs:null Results: {
"PatchDiff": "commit 09d9f620790fc29eeca00f59ba9b6cad5e0ad62c\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate: Mon Jun 22 09:40:55 2026 +0000\n\n syz-cluster: applied patch under review\n\ndiff --git a/include/net/flow_offload.h b/include/net/flow_offload.h\nindex 70a02ee1430807..8d97a5f293e6d9 100644\n--- a/include/net/flow_offload.h\n+++ b/include/net/flow_offload.h\n@@ -184,6 +184,7 @@ enum flow_action_id {\n \tFLOW_ACTION_VLAN_PUSH_ETH,\n \tFLOW_ACTION_VLAN_POP_ETH,\n \tFLOW_ACTION_CONTINUE,\n+\tFLOW_ACTION_FRER,\n \tNUM_FLOW_ACTIONS,\n };\n \n@@ -329,6 +330,16 @@ struct flow_action_entry {\n \t\tstruct {\t\t\t\t/* FLOW_ACTION_PPPOE_PUSH */\n \t\t\tu16\t\tsid;\n \t\t} pppoe;\n+\t\tstruct { /* FLOW_ACTION_FRER */\n+\t\t\tu8\t\tfunc;\n+\t\t\tu8\t\ttag_type;\n+\t\t\tbool\t\tindividual;\n+\t\t\tu8\t\trcvy_alg;\n+\t\t\tu8\t\trcvy_history_len;\n+\t\t\tu32\t\trcvy_reset_msec;\n+\t\t\tbool\t\ttag_pop;\n+\t\t\tbool\t\ttake_no_seq;\n+\t\t} frer;\n \t};\n \tstruct flow_action_cookie *user_cookie; /* user defined action cookie */\n };\ndiff --git a/include/net/tc_act/tc_frer.h b/include/net/tc_act/tc_frer.h\nnew file mode 100644\nindex 00000000000000..5f6f8ca70813a0\n--- /dev/null\n+++ b/include/net/tc_act/tc_frer.h\n@@ -0,0 +1,71 @@\n+/* SPDX-License-Identifier: GPL-2.0-or-later */\n+/* Copyright 2026 NXP */\n+\n+#ifndef __NET_TC_FRER_H\n+#define __NET_TC_FRER_H\n+\n+#include \u003cnet/act_api.h\u003e\n+#include \u003clinux/tc_act/tc_frer.h\u003e\n+\n+/**\n+ * struct frer_seqgen - sequence number generator state (embedded in tcf_frer)\n+ */\n+struct frer_seqgen {\n+\tu32\t\tgen_seq_num;\n+\tu64\t\tseq_space;\t/* 1 \u003c\u003c 16 */\n+\tspinlock_t\tlock;\t\t/* protects frer_seqgen state */\n+\tu64\t\tstats_pkts;\t/* frerCpsSeqGenPackets */\n+};\n+\n+/**\n+ * struct frer_rcvy - sequence recovery state (embedded in tcf_frer)\n+ *\n+ */\n+struct frer_rcvy {\n+\tu8\t\talg;\n+\tu8\t\thistory_len;\t/* 1-32 */\n+\tu32\t\treset_msec;\n+\tu64\t\tseq_space;\n+\tu32\t\trcvy_seq_num;\n+\tu32\t\tseq_history;\n+\tbool\t\ttake_any;\n+\tbool\t\ttake_no_seq;\n+\tstruct hrtimer\thrtimer;\n+\tspinlock_t\tlock;\t\t/* protects frer_rcvy state */\n+\t/* statistics */\n+\tu64\t\tstats_tagless_pkts;\n+\tu64\t\tstats_out_of_order_pkts;\n+\tu64\t\tstats_rogue_pkts;\n+\tu64\t\tstats_lost_pkts;\n+\tu64\t\tstats_resets;\n+\tu64\t\tstats_passed_pkts;\n+\tu64\t\tstats_discarded_pkts;\n+};\n+\n+/**\n+ * struct tcf_frer - per tc_action FRER private data\n+ */\n+struct tcf_frer {\n+\tstruct tc_action\tcommon;\n+\tu8\t\t\tfunc;\n+\tu8\t\t\ttag_type;\n+\tbool\t\t\ttag_pop;\n+\tbool\t\t\tindividual;\t/* Individual Recovery flag */\n+\t/* push path */\n+\tstruct frer_seqgen\tseqgen;\n+\t/* recover path */\n+\tstruct frer_rcvy\trcvy;\n+};\n+\n+#define to_frer(a) ((struct tcf_frer *)(a))\n+\n+static inline bool is_tcf_frer(const struct tc_action *a)\n+{\n+#ifdef CONFIG_NET_CLS_ACT\n+\tif (a-\u003eops \u0026\u0026 a-\u003eops-\u003eid == TCA_ID_FRER)\n+\t\treturn true;\n+#endif\n+\treturn false;\n+}\n+\n+#endif /* __NET_TC_FRER_H */\ndiff --git a/include/uapi/linux/if_ether.h b/include/uapi/linux/if_ether.h\nindex fb5efc8e06cc14..2d909078cde18d 100644\n--- a/include/uapi/linux/if_ether.h\n+++ b/include/uapi/linux/if_ether.h\n@@ -122,6 +122,7 @@\n #define ETH_P_DSA_8021Q\t0xDADB\t\t/* Fake VLAN Header for DSA [ NOT AN OFFICIALLY REGISTERED ID ] */\n #define ETH_P_DSA_A5PSW\t0xE001\t\t/* A5PSW Tag Value [ NOT AN OFFICIALLY REGISTERED ID ] */\n #define ETH_P_IFE\t0xED3E\t\t/* ForCES inter-FE LFB type */\n+#define ETH_P_RTAG\t0xF1C1\t\t/* Redundancy Tag (IEEE 802.1CB) */\n #define ETH_P_AF_IUCV 0xFBFB\t\t/* IBM af_iucv [ NOT AN OFFICIALLY REGISTERED ID ] */\n #define ETH_P_NXP_NETC 0xFD3A\t\t/* NXP NETC DSA [ NOT AN OFFICIALLY REGISTERED ID ] */\n \ndiff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h\nindex 28d94b11d1aa16..9b87f0455110d3 100644\n--- a/include/uapi/linux/pkt_cls.h\n+++ b/include/uapi/linux/pkt_cls.h\n@@ -139,6 +139,7 @@ enum tca_id {\n \tTCA_ID_MPLS,\n \tTCA_ID_CT,\n \tTCA_ID_GATE,\n+\tTCA_ID_FRER,\n \t/* other actions go here */\n \t__TCA_ID_MAX = 255\n };\ndiff --git a/include/uapi/linux/tc_act/tc_frer.h b/include/uapi/linux/tc_act/tc_frer.h\nnew file mode 100644\nindex 00000000000000..241e90827e2628\n--- /dev/null\n+++ b/include/uapi/linux/tc_act/tc_frer.h\n@@ -0,0 +1,89 @@\n+/* SPDX-License-Identifier: GPL-2.0+ WITH Linux-syscall-note */\n+/* Copyright 2026 NXP */\n+\n+#ifndef __LINUX_TC_FRER_H\n+#define __LINUX_TC_FRER_H\n+\n+#include \u003clinux/pkt_cls.h\u003e\n+\n+/* Base parameters passed in TCA_FRER_PARMS */\n+struct tc_frer {\n+\ttc_gen;\n+};\n+\n+/**\n+ * enum TCA_FRER_* - netlink attributes for the FRER tc action\n+ *\n+ * @TCA_FRER_FUNC: Functional sub-command (tc_frer_func).\n+ * Mandatory.\n+ * @TCA_FRER_TAG_TYPE: Redundancy tag type (tc_frer_tag_type).\n+ * Mandatory.\n+ *\n+ * Push-specific attributes (TCA_FRER_FUNC_PUSH):\n+ * Recover-specific attributes (TCA_FRER_FUNC_RECOVER):\n+ * @TCA_FRER_RCVY_INDIVIDUAL: Flag. Force Individual Recovery.\n+ * @TCA_FRER_RCVY_ALG: u8. Recovery algorithm (tc_frer_rcvy_alg).\n+ * @TCA_FRER_RCVY_HISTORY_LEN: u8. SequenceHistory window size (1-32).\n+ * Maps to frerSeqRcvyHistoryLength.\n+ * @TCA_FRER_RCVY_RESET_MSEC: u32. Reset timer in milliseconds.\n+ * 0 disables the timer.\n+ * Maps to frerSeqRcvyResetMSec.\n+ * @TCA_FRER_RCVY_TAKE_NO_SEQ: Flag. Accept frames without a redundancy\n+ * tag and pass them unconditionally.\n+ * Maps to frerSeqRcvyTakeNoSeq.\n+ * @TCA_FRER_RCVY_TAG_POP: Flag. Remove the redundancy tag from\n+ * frames that pass the recovery function.\n+ *\n+ * Read-only statistics (filled on dump, IEEE 802.1CB Table 10-1):\n+ * @TCA_FRER_STATS_TAGLESS_PKTS: frerCpsSeqRcvyTaglessPackets\n+ * @TCA_FRER_STATS_OUT_OF_ORDER_PKTS: frerCpsSeqRcvyOutOfOrderPackets\n+ * @TCA_FRER_STATS_ROGUE_PKTS: frerCpsSeqRcvyRoguePackets\n+ * @TCA_FRER_STATS_LOST_PKTS: frerCpsSeqRcvyLostPackets\n+ * @TCA_FRER_STATS_RESETS: frerCpsSeqRcvyResets\n+ * @TCA_FRER_STATS_PASSED_PKTS: frerCpsSeqRcvyPassedPackets\n+ * @TCA_FRER_STATS_DISCARDED_PKTS: frerCpsSeqRcvyDiscardedPackets\n+ * @TCA_FRER_STATS_SEQGEN_PKTS: frerCpsSeqGenPackets\n+ */\n+enum {\n+\tTCA_FRER_UNSPEC,\n+\tTCA_FRER_TM, /* struct tcf_t */\n+\tTCA_FRER_PARMS, /* struct tc_frer */\n+\tTCA_FRER_PAD,\n+\tTCA_FRER_FUNC, /* u8: tc_frer_func */\n+\tTCA_FRER_TAG_TYPE, /* u8: tc_frer_tag_type */\n+\tTCA_FRER_RCVY_INDIVIDUAL, /* NLA_FLAG */\n+\tTCA_FRER_RCVY_ALG, /* u8: tc_frer_rcvy_alg */\n+\tTCA_FRER_RCVY_HISTORY_LEN, /* u8: 1-32 */\n+\tTCA_FRER_RCVY_RESET_MSEC, /* u32 */\n+\tTCA_FRER_RCVY_TAKE_NO_SEQ, /* NLA_FLAG */\n+\tTCA_FRER_RCVY_TAG_POP, /* NLA_FLAG */\n+\tTCA_FRER_STATS_TAGLESS_PKTS, /* u64 */\n+\tTCA_FRER_STATS_OUT_OF_ORDER_PKTS, /* u64 */\n+\tTCA_FRER_STATS_ROGUE_PKTS, /* u64 */\n+\tTCA_FRER_STATS_LOST_PKTS, /* u64 */\n+\tTCA_FRER_STATS_RESETS, /* u64 */\n+\tTCA_FRER_STATS_PASSED_PKTS, /* u64 */\n+\tTCA_FRER_STATS_DISCARDED_PKTS, /* u64 */\n+\tTCA_FRER_STATS_SEQGEN_PKTS, /* u64 */\n+\t__TCA_FRER_MAX,\n+};\n+\n+#define TCA_FRER_MAX (__TCA_FRER_MAX - 1)\n+\n+enum tc_frer_func {\n+\tTCA_FRER_FUNC_PUSH = 1,\n+\tTCA_FRER_FUNC_RECOVER = 2,\n+};\n+\n+enum tc_frer_tag_type {\n+\tTCA_FRER_TAG_RTAG = 1,\n+\tTCA_FRER_TAG_HSR,\n+\tTCA_FRER_TAG_PRP,\n+};\n+\n+enum tc_frer_rcvy_alg {\n+\tTCA_FRER_RCVY_VECTOR_ALG = 0, /* IEEE 802.1CB 7.4.3.4 */\n+\tTCA_FRER_RCVY_MATCH_ALG = 1, /* IEEE 802.1CB 7.4.3.5 */\n+};\n+\n+#endif /* __LINUX_TC_FRER_H */\ndiff --git a/net/sched/Kconfig b/net/sched/Kconfig\nindex 6ddff028b81a41..7ca79b3eb5b34c 100644\n--- a/net/sched/Kconfig\n+++ b/net/sched/Kconfig\n@@ -939,6 +939,22 @@ config NET_ACT_GATE\n \t To compile this code as a module, choose M here: the\n \t module will be called act_gate.\n \n+config NET_ACT_FRER\n+\ttristate \"IEEE 802.1CB FRER tc action\"\n+\tdepends on NET_CLS_ACT\n+\thelp\n+\t Say Y here to enable the IEEE 802.1CB FRER tc action. The action\n+\t implements the Sequence Generation Function (egress R-TAG insertion\n+\t with shared per-stream sequence counter) and the Sequence Recovery\n+\t Function (ingress duplicate detection and elimination) described in\n+\t IEEE 802.1CB-2017.\n+\n+\t Both Sequence Recovery (cross-port shared state via rcvy-id) and\n+\t Individual Recovery (per-port independent state) are supported.\n+\n+\t To compile this code as a module, choose M here: the\n+\t module will be called act_frer.\n+\n config NET_IFE_SKBMARK\n \ttristate \"Support to encoding decoding skb mark on IFE action\"\n \tdepends on NET_ACT_IFE\ndiff --git a/net/sched/Makefile b/net/sched/Makefile\nindex 5078ea84e6ad7a..d9f60434e7d77f 100644\n--- a/net/sched/Makefile\n+++ b/net/sched/Makefile\n@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_IFE_SKBTCINDEX)\t+= act_meta_skbtcindex.o\n obj-$(CONFIG_NET_ACT_TUNNEL_KEY)+= act_tunnel_key.o\n obj-$(CONFIG_NET_ACT_CT)\t+= act_ct.o\n obj-$(CONFIG_NET_ACT_GATE)\t+= act_gate.o\n+obj-$(CONFIG_NET_ACT_FRER)\t+= act_frer.o\n obj-$(CONFIG_NET_SCH_FIFO)\t+= sch_fifo.o\n obj-$(CONFIG_NET_SCH_HTB)\t+= sch_htb.o\n obj-$(CONFIG_NET_SCH_HFSC)\t+= sch_hfsc.o\ndiff --git a/net/sched/act_frer.c b/net/sched/act_frer.c\nnew file mode 100644\nindex 00000000000000..7b6db643788df7\n--- /dev/null\n+++ b/net/sched/act_frer.c\n@@ -0,0 +1,835 @@\n+// SPDX-License-Identifier: GPL-2.0-or-later\n+/* Copyright 2026 NXP */\n+\n+#include \u003clinux/module.h\u003e\n+#include \u003clinux/types.h\u003e\n+#include \u003clinux/kernel.h\u003e\n+#include \u003clinux/skbuff.h\u003e\n+#include \u003clinux/rtnetlink.h\u003e\n+#include \u003clinux/slab.h\u003e\n+#include \u003clinux/spinlock.h\u003e\n+#include \u003clinux/if_vlan.h\u003e\n+#include \u003clinux/hrtimer.h\u003e\n+#include \u003clinux/workqueue.h\u003e\n+#include \u003cnet/act_api.h\u003e\n+#include \u003cnet/netlink.h\u003e\n+#include \u003cnet/pkt_cls.h\u003e\n+#include \u003cnet/tc_act/tc_frer.h\u003e\n+\n+/* ------------------------------------------------------------------ */\n+/* R-TAG wire structures (IEEE 802.1CB 7.8) */\n+/* ------------------------------------------------------------------ */\n+\n+struct r_tag {\n+\t__be16 reserved;\n+\t__be16 sequence_nr;\n+\t__be16 encap_proto;\n+} __packed;\n+\n+static struct tc_action_ops act_frer_ops;\n+\n+/* ------------------------------------------------------------------ */\n+/* Recovery reset machinery */\n+/* ------------------------------------------------------------------ */\n+\n+struct frer_rcvy_work {\n+\tstruct work_struct\twork;\n+\tstruct frer_rcvy\t*rcvy;\n+};\n+\n+static void frer_rcvy_reset(struct frer_rcvy *rcvy)\n+{\n+\tif (rcvy-\u003ealg == TCA_FRER_RCVY_VECTOR_ALG) {\n+\t\trcvy-\u003ercvy_seq_num = (u32)(rcvy-\u003eseq_space - 1);\n+\t\trcvy-\u003eseq_history = 0;\n+\t}\n+\trcvy-\u003etake_any = true;\n+\trcvy-\u003estats_resets++;\n+}\n+\n+static void frer_rcvy_reset_work_fn(struct work_struct *work)\n+{\n+\tstruct frer_rcvy_work *rw =\n+\t\tcontainer_of(work, struct frer_rcvy_work, work);\n+\tstruct frer_rcvy *rcvy = rw-\u003ercvy;\n+\n+\tspin_lock_bh(\u0026rcvy-\u003elock);\n+\tfrer_rcvy_reset(rcvy);\n+\tspin_unlock_bh(\u0026rcvy-\u003elock);\n+\tkfree(rw);\n+}\n+\n+static enum hrtimer_restart frer_rcvy_hrtimer_fn(struct hrtimer *timer)\n+{\n+\tstruct frer_rcvy *rcvy =\n+\t\tcontainer_of(timer, struct frer_rcvy, hrtimer);\n+\tstruct frer_rcvy_work *rw;\n+\n+\t/* Allocate in GFP_ATOMIC context; if it fails the state is not\n+\t * reset this cycle - the next frame will attempt again.\n+\t */\n+\trw = kmalloc_obj(*rw);\n+\tif (rw) {\n+\t\tINIT_WORK(\u0026rw-\u003ework, frer_rcvy_reset_work_fn);\n+\t\trw-\u003ercvy = rcvy;\n+\t\tschedule_work(\u0026rw-\u003ework);\n+\t}\n+\treturn HRTIMER_NORESTART;\n+}\n+\n+static void frer_rcvy_timer_restart(struct frer_rcvy *rcvy)\n+{\n+\tif (rcvy-\u003ereset_msec)\n+\t\thrtimer_start(\u0026rcvy-\u003ehrtimer,\n+\t\t\t ms_to_ktime(rcvy-\u003ereset_msec),\n+\t\t\t HRTIMER_MODE_REL_SOFT);\n+}\n+\n+static void frer_rcvy_init_state(struct frer_rcvy *rcvy, u8 alg,\n+\t\t\t\t u8 history_len, u32 reset_msec,\n+\t\t\t\t bool take_no_seq)\n+{\n+\trcvy-\u003ealg = alg;\n+\trcvy-\u003ehistory_len = history_len;\n+\trcvy-\u003ereset_msec = reset_msec;\n+\trcvy-\u003eseq_space = 1 \u003c\u003c 16;\n+\trcvy-\u003etake_no_seq = take_no_seq;\n+\trcvy-\u003etake_any = true;\n+\trcvy-\u003ercvy_seq_num = (u32)(rcvy-\u003eseq_space - 1);\n+\trcvy-\u003eseq_history = 0;\n+\tspin_lock_init(\u0026rcvy-\u003elock);\n+\thrtimer_setup(\u0026rcvy-\u003ehrtimer, frer_rcvy_hrtimer_fn, CLOCK_MONOTONIC,\n+\t\t HRTIMER_MODE_REL_SOFT);\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* R-TAG helpers */\n+/* ------------------------------------------------------------------ */\n+\n+static int frer_rtag_push(struct sk_buff *skb, u16 seq_num)\n+{\n+\tunsigned char *new_mac_header;\n+\tunsigned int data_offset;\n+\tunsigned int head_len;\n+\tstruct vlan_ethhdr *vh;\n+\tstruct ethhdr *eh;\n+\tstruct r_tag *rtag;\n+\t__be16 *proto_ptr;\n+\t__be16 saved_proto;\n+\n+\tif (!skb_mac_header_was_set(skb))\n+\t\treturn -EINVAL;\n+\n+\tdata_offset = skb-\u003edata - skb_mac_header(skb);\n+\n+\tif (skb_cow_head(skb, data_offset + sizeof(*rtag)))\n+\t\treturn -ENOMEM;\n+\n+\tif (data_offset \u003e 0)\n+\t\tskb_push(skb, data_offset);\n+\n+\teh = eth_hdr(skb);\n+\tif (eth_type_vlan(eh-\u003eh_proto)) {\n+\t\tif (!pskb_may_pull(skb, sizeof(*vh)))\n+\t\t\treturn -EINVAL;\n+\t\teh = eth_hdr(skb);\n+\t\tvh = (struct vlan_ethhdr *)eh;\n+\t\tproto_ptr = \u0026vh-\u003eh_vlan_encapsulated_proto;\n+\t\thead_len = sizeof(*vh);\n+\t} else {\n+\t\tif (!pskb_may_pull(skb, sizeof(*eh)))\n+\t\t\treturn -EINVAL;\n+\t\teh = eth_hdr(skb);\n+\t\tproto_ptr = \u0026eh-\u003eh_proto;\n+\t\thead_len = sizeof(*eh);\n+\t}\n+\n+\tsaved_proto = *proto_ptr;\n+\t*proto_ptr = htons(ETH_P_RTAG);\n+\n+\tskb_push(skb, sizeof(*rtag));\n+\tskb_reset_mac_header(skb);\n+\n+\tnew_mac_header = skb_mac_header(skb);\n+\tmemmove(new_mac_header, (unsigned char *)eh, head_len);\n+\n+\tskb-\u003eprotocol = htons(ETH_P_RTAG);\n+\tskb_set_network_header(skb, head_len);\n+\tif (data_offset \u003e 0)\n+\t\tskb_pull(skb, data_offset);\n+\n+\t/* Write R-TAG after the Ethernet / VLAN header */\n+\trtag = (struct r_tag *)(new_mac_header + head_len);\n+\trtag-\u003ereserved = 0;\n+\trtag-\u003esequence_nr = htons(seq_num);\n+\trtag-\u003eencap_proto = saved_proto;\n+\n+\treturn 0;\n+}\n+\n+static void frer_rtag_pop(struct sk_buff *skb)\n+{\n+\tunsigned char *new_mac_header;\n+\tunsigned int data_offset;\n+\tunsigned int head_len;\n+\tstruct vlan_ethhdr *vh;\n+\tstruct ethhdr *eh;\n+\tstruct r_tag *rtag;\n+\t__be16 *proto_ptr;\n+\n+\tdata_offset = skb-\u003edata - skb_mac_header(skb);\n+\tif (data_offset \u003e 0)\n+\t\tskb_push(skb, data_offset);\n+\n+\teh = eth_hdr(skb);\n+\tif (eth_type_vlan(eh-\u003eh_proto)) {\n+\t\tvh = (struct vlan_ethhdr *)eh;\n+\t\tproto_ptr = \u0026vh-\u003eh_vlan_encapsulated_proto;\n+\t\thead_len = sizeof(*vh);\n+\t} else {\n+\t\tproto_ptr = \u0026eh-\u003eh_proto;\n+\t\thead_len = sizeof(*eh);\n+\t}\n+\n+\tif (*proto_ptr != htons(ETH_P_RTAG))\n+\t\treturn;\n+\n+\trtag = (struct r_tag *)((unsigned char *)eh + head_len);\n+\t*proto_ptr = rtag-\u003eencap_proto;\n+\n+\tskb-\u003eprotocol = rtag-\u003eencap_proto;\n+\n+\tskb_postpull_rcsum(skb, rtag, sizeof(struct r_tag));\n+\tskb_pull(skb, sizeof(*rtag));\n+\tskb_reset_mac_header(skb);\n+\n+\tnew_mac_header = skb_mac_header(skb);\n+\tmemmove(new_mac_header, (unsigned char *)eh, head_len);\n+\n+\tskb_set_network_header(skb, head_len);\n+\tif (data_offset \u003e 0)\n+\t\tskb_pull(skb, data_offset);\n+}\n+\n+static int frer_rtag_decode(struct sk_buff *skb, int *seq)\n+{\n+\tunsigned int data_offset;\n+\tstruct vlan_ethhdr *vh;\n+\tunsigned int head_len;\n+\tstruct ethhdr *eh;\n+\tstruct r_tag *rtag;\n+\t__be16 *proto_ptr;\n+\n+\tif (!skb_mac_header_was_set(skb))\n+\t\treturn -EINVAL;\n+\n+\tdata_offset = skb-\u003edata - skb_mac_header(skb);\n+\n+\tif (skb_cow_head(skb, data_offset))\n+\t\treturn -ENOMEM;\n+\n+\tif (data_offset \u003e 0)\n+\t\tskb_push(skb, data_offset);\n+\n+\teh = eth_hdr(skb);\n+\tif (eth_type_vlan(eh-\u003eh_proto)) {\n+\t\tif (!pskb_may_pull(skb, sizeof(*vh) + sizeof(*rtag)))\n+\t\t\treturn -EINVAL;\n+\t\teh = eth_hdr(skb);\n+\t\tvh = (struct vlan_ethhdr *)eh;\n+\t\tproto_ptr = \u0026vh-\u003eh_vlan_encapsulated_proto;\n+\t\thead_len = sizeof(*vh);\n+\t} else {\n+\t\tif (!pskb_may_pull(skb, sizeof(*eh) + sizeof(*rtag)))\n+\t\t\treturn -EINVAL;\n+\t\teh = eth_hdr(skb);\n+\t\tproto_ptr = \u0026eh-\u003eh_proto;\n+\t\thead_len = sizeof(*eh);\n+\t}\n+\n+\tif (data_offset \u003e 0)\n+\t\tskb_pull(skb, data_offset);\n+\n+\tif (*proto_ptr != htons(ETH_P_RTAG)) {\n+\t\t*seq = -1;\n+\t\treturn 0;\n+\t}\n+\n+\trtag = (struct r_tag *)((unsigned char *)eh + head_len);\n+\n+\t*seq = (int)ntohs(rtag-\u003esequence_nr);\n+\n+\treturn 0;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Recovery algorithms (called with rcvy-\u003elock held) */\n+/* ------------------------------------------------------------------ */\n+\n+/* Returns true = pass frame, false = discard frame.\n+ * @individual: when true, restart the reset timer even on discarded frames\n+ * (rogue/duplicate), as required for Individual Recovery (IEEE 802.1CB 7.5).\n+ */\n+static bool frer_vector_alg(struct frer_rcvy *rcvy, int seq, bool individual)\n+{\n+\tint delta;\n+\tbool restart_timer = false;\n+\tbool pass;\n+\n+\tif (seq \u003c 0) {\n+\t\t/* No R-TAG present */\n+\t\trcvy-\u003estats_tagless_pkts++;\n+\t\tif (rcvy-\u003etake_no_seq) {\n+\t\t\trestart_timer = true;\n+\t\t\tpass = true;\n+\t\t} else {\n+\t\t\tpass = false;\n+\t\t}\n+\t\tgoto out;\n+\t}\n+\n+\tif (rcvy-\u003etake_any) {\n+\t\t/* First frame after reset: accept unconditionally */\n+\t\trcvy-\u003etake_any = false;\n+\t\trcvy-\u003ercvy_seq_num = (u32)seq;\n+\t\trcvy-\u003eseq_history = BIT(0);\n+\t\trestart_timer = true;\n+\t\tpass = true;\n+\t\tgoto out;\n+\t}\n+\n+\tdelta = (seq - (int)rcvy-\u003ercvy_seq_num) \u0026\n+\t\t(int)(rcvy-\u003eseq_space - 1);\n+\t/* Map delta \u003e seq_space/2 to negative (signed wrap) */\n+\tif ((u32)delta \u0026 (u32)(rcvy-\u003eseq_space / 2))\n+\t\tdelta -= (int)rcvy-\u003eseq_space;\n+\n+\tif (delta \u003e= (int)rcvy-\u003ehistory_len ||\n+\t delta \u003c= -(int)rcvy-\u003ehistory_len) {\n+\t\t/* Packet is out-of-range (rogue). */\n+\t\trcvy-\u003estats_rogue_pkts++;\n+\t\tif (individual)\n+\t\t\trestart_timer = true;\n+\t\tpass = false;\n+\t\tgoto out;\n+\t}\n+\n+\tif (delta \u003c= 0) {\n+\t\t/* Packet is old: check whether already seen. */\n+\t\tif (rcvy-\u003eseq_history \u0026 BIT(-delta)) {\n+\t\t\tif (individual)\n+\t\t\t\trestart_timer = true;\n+\t\t\t/* Already received */\n+\t\t\tpass = false;\n+\t\t} else {\n+\t\t\t/* Out-of-order but not yet seen */\n+\t\t\trcvy-\u003eseq_history |= BIT(-delta);\n+\t\t\trcvy-\u003estats_out_of_order_pkts++;\n+\t\t\trestart_timer = true;\n+\t\t\tpass = true;\n+\t\t}\n+\t\tgoto out;\n+\t}\n+\n+\t/* delta \u003e 0: frame is newer than expected */\n+\tif (delta != 1)\n+\t\trcvy-\u003estats_out_of_order_pkts++;\n+\n+\t/* Shift history forward, counting any gaps as lost */\n+\twhile (--delta) {\n+\t\tif (!(rcvy-\u003eseq_history \u0026 BIT(rcvy-\u003ehistory_len - 1)))\n+\t\t\trcvy-\u003estats_lost_pkts++;\n+\t\trcvy-\u003eseq_history \u003c\u003c= 1;\n+\t}\n+\tif (!(rcvy-\u003eseq_history \u0026 BIT(rcvy-\u003ehistory_len - 1)))\n+\t\trcvy-\u003estats_lost_pkts++;\n+\trcvy-\u003eseq_history = (rcvy-\u003eseq_history \u003c\u003c 1) | BIT(0);\n+\trcvy-\u003ercvy_seq_num = (u32)seq;\n+\trestart_timer = true;\n+\tpass = true;\n+\n+out:\n+\tif (restart_timer)\n+\t\tfrer_rcvy_timer_restart(rcvy);\n+\treturn pass;\n+}\n+\n+static bool frer_match_alg(struct frer_rcvy *rcvy, int seq, bool individual)\n+{\n+\tif (seq \u003c 0) {\n+\t\t/* No R-TAG: Match alg cannot deduplicate, always pass. */\n+\t\trcvy-\u003estats_tagless_pkts++;\n+\t\treturn true;\n+\t}\n+\n+\tif (rcvy-\u003etake_any) {\n+\t\trcvy-\u003etake_any = false;\n+\t\trcvy-\u003ercvy_seq_num = (u32)seq;\n+\t\tfrer_rcvy_timer_restart(rcvy);\n+\t\treturn true;\n+\t}\n+\n+\tif ((u32)seq == rcvy-\u003ercvy_seq_num) {\n+\t\t/* Duplicate */\n+\t\tif (individual)\n+\t\t\tfrer_rcvy_timer_restart(rcvy);\n+\t\treturn false;\n+\t}\n+\n+\t/* New sequence number: accept and update */\n+\tif ((u32)seq != ((rcvy-\u003ercvy_seq_num + 1) % rcvy-\u003eseq_space))\n+\t\trcvy-\u003estats_out_of_order_pkts++;\n+\trcvy-\u003ercvy_seq_num = (u32)seq;\n+\tfrer_rcvy_timer_restart(rcvy);\n+\treturn true;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Netlink policy */\n+/* ------------------------------------------------------------------ */\n+\n+static const struct nla_policy frer_policy[TCA_FRER_MAX + 1] = {\n+\t[TCA_FRER_PARMS] = NLA_POLICY_EXACT_LEN(sizeof(struct tc_frer)),\n+\t[TCA_FRER_FUNC] = { .type = NLA_U8 },\n+\t[TCA_FRER_TAG_TYPE] = { .type = NLA_U8 },\n+\t[TCA_FRER_RCVY_INDIVIDUAL] = { .type = NLA_FLAG },\n+\t[TCA_FRER_RCVY_ALG] = { .type = NLA_U8 },\n+\t[TCA_FRER_RCVY_HISTORY_LEN] = NLA_POLICY_RANGE(NLA_U8, 1, 32),\n+\t[TCA_FRER_RCVY_RESET_MSEC] = { .type = NLA_U32 },\n+\t[TCA_FRER_RCVY_TAKE_NO_SEQ] = { .type = NLA_FLAG },\n+\t[TCA_FRER_RCVY_TAG_POP] = { .type = NLA_FLAG },\n+};\n+\n+/* ------------------------------------------------------------------ */\n+/* Action init */\n+/* ------------------------------------------------------------------ */\n+\n+static int tcf_frer_init(struct net *net, struct nlattr *nla,\n+\t\t\t struct nlattr *est, struct tc_action **a,\n+\t\t\t struct tcf_proto *tp, u32 flags,\n+\t\t\t struct netlink_ext_ack *extack)\n+{\n+\tstruct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);\n+\tbool bind = flags \u0026 TCA_ACT_FLAGS_BIND;\n+\tstruct nlattr *tb[TCA_FRER_MAX + 1];\n+\tstruct tcf_chain *goto_ch = NULL;\n+\tstruct tcf_frer *f;\n+\tstruct tc_frer *parm;\n+\tbool exists = false;\n+\tint ret = 0, err, index;\n+\tu8 func, tag_type;\n+\n+\tif (!nla) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: attributes required\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\terr = nla_parse_nested(tb, TCA_FRER_MAX, nla, frer_policy, extack);\n+\tif (err \u003c 0)\n+\t\treturn err;\n+\n+\tif (!tb[TCA_FRER_PARMS]) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: TCA_FRER_PARMS missing\");\n+\t\treturn -EINVAL;\n+\t}\n+\tif (!tb[TCA_FRER_FUNC]) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: TCA_FRER_FUNC missing\");\n+\t\treturn -EINVAL;\n+\t}\n+\tif (!tb[TCA_FRER_TAG_TYPE]) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: TCA_FRER_TAG_TYPE missing\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tfunc = nla_get_u8(tb[TCA_FRER_FUNC]);\n+\ttag_type = nla_get_u8(tb[TCA_FRER_TAG_TYPE]);\n+\n+\tif (func != TCA_FRER_FUNC_PUSH \u0026\u0026 func != TCA_FRER_FUNC_RECOVER) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: unknown func\");\n+\t\treturn -EINVAL;\n+\t}\n+\tif (tag_type != TCA_FRER_TAG_RTAG) {\n+\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: only rtag supported\");\n+\t\treturn -EOPNOTSUPP;\n+\t}\n+\n+\tparm = nla_data(tb[TCA_FRER_PARMS]);\n+\tindex = parm-\u003eindex;\n+\n+\terr = tcf_idr_check_alloc(tn, \u0026index, a, bind);\n+\tif (err \u003c 0)\n+\t\treturn err;\n+\texists = err;\n+\n+\tif (exists \u0026\u0026 bind)\n+\t\treturn ACT_P_BOUND;\n+\n+\tif (!exists) {\n+\t\tret = tcf_idr_create_from_flags(tn, index, est, a,\n+\t\t\t\t\t\t\u0026act_frer_ops, bind, flags);\n+\t\tif (ret) {\n+\t\t\ttcf_idr_cleanup(tn, index);\n+\t\t\treturn ret;\n+\t\t}\n+\t\tret = ACT_P_CREATED;\n+\t} else if (!(flags \u0026 TCA_ACT_FLAGS_REPLACE)) {\n+\t\ttcf_idr_release(*a, bind);\n+\t\treturn -EEXIST;\n+\t}\n+\n+\terr = tcf_action_check_ctrlact(parm-\u003eaction, tp, \u0026goto_ch, extack);\n+\tif (err \u003c 0)\n+\t\tgoto release_idr;\n+\n+\tf = to_frer(*a);\n+\n+\tspin_lock_bh(\u0026f-\u003etcf_lock);\n+\tgoto_ch = tcf_action_set_ctrlact(*a, parm-\u003eaction, goto_ch);\n+\tf-\u003efunc = func;\n+\tf-\u003etag_type = tag_type;\n+\tf-\u003etag_pop = !!tb[TCA_FRER_RCVY_TAG_POP];\n+\n+\tif (func == TCA_FRER_FUNC_PUSH) {\n+\t\tif (ret == ACT_P_CREATED) {\n+\t\t\tspin_lock_init(\u0026f-\u003eseqgen.lock);\n+\t\t\tf-\u003eseqgen.seq_space = 1 \u003c\u003c 16;\n+\t\t}\n+\t\t/* gen_seq_num starts at 0 on creation; preserved on replace */\n+\t} else {\n+\t\tu8 alg = tb[TCA_FRER_RCVY_ALG] ?\n+\t\t\t nla_get_u8(tb[TCA_FRER_RCVY_ALG]) :\n+\t\t\t TCA_FRER_RCVY_VECTOR_ALG;\n+\t\tu8 history_len = tb[TCA_FRER_RCVY_HISTORY_LEN] ?\n+\t\t\t\t nla_get_u8(tb[TCA_FRER_RCVY_HISTORY_LEN]) : 32;\n+\t\tu32 reset_msec = tb[TCA_FRER_RCVY_RESET_MSEC] ?\n+\t\t\t\t nla_get_u32(tb[TCA_FRER_RCVY_RESET_MSEC]) : 0;\n+\t\tbool take_no_seq = !!tb[TCA_FRER_RCVY_TAKE_NO_SEQ];\n+\n+\t\tif (alg != TCA_FRER_RCVY_VECTOR_ALG \u0026\u0026\n+\t\t alg != TCA_FRER_RCVY_MATCH_ALG) {\n+\t\t\tspin_unlock_bh(\u0026f-\u003etcf_lock);\n+\t\t\tNL_SET_ERR_MSG_MOD(extack, \"frer: unknown recovery algorithm\");\n+\t\t\terr = -EINVAL;\n+\t\t\tgoto release_idr;\n+\t\t}\n+\n+\t\tf-\u003eindividual = !!tb[TCA_FRER_RCVY_INDIVIDUAL];\n+\n+\t\t/* Cancel any running reset timer before re-initialising. */\n+\t\tif (ret != ACT_P_CREATED \u0026\u0026 f-\u003ercvy.reset_msec) {\n+\t\t\tspin_unlock_bh(\u0026f-\u003etcf_lock);\n+\t\t\thrtimer_cancel(\u0026f-\u003ercvy.hrtimer);\n+\t\t\tspin_lock_bh(\u0026f-\u003etcf_lock);\n+\t\t}\n+\n+\t\tfrer_rcvy_init_state(\u0026f-\u003ercvy, alg, history_len,\n+\t\t\t\t reset_msec, take_no_seq);\n+\t}\n+\n+\tspin_unlock_bh(\u0026f-\u003etcf_lock);\n+\n+\tif (goto_ch)\n+\t\ttcf_chain_put_by_act(goto_ch);\n+\n+\treturn ret;\n+\n+release_idr:\n+\ttcf_idr_release(*a, bind);\n+\treturn err;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Data path */\n+/* ------------------------------------------------------------------ */\n+\n+static int tcf_frer_act(struct sk_buff *skb, const struct tc_action *a,\n+\t\t\tstruct tcf_result *res)\n+{\n+\tstruct tcf_frer *f = to_frer(a);\n+\tint retval;\n+\n+\ttcf_lastuse_update(\u0026f-\u003etcf_tm);\n+\ttcf_action_update_bstats(\u0026f-\u003ecommon, skb);\n+\tretval = READ_ONCE(f-\u003etcf_action);\n+\n+\tif (f-\u003efunc == TCA_FRER_FUNC_PUSH) {\n+\t\tstruct frer_seqgen *sg = \u0026f-\u003eseqgen;\n+\t\tu16 seq;\n+\n+\t\tspin_lock(\u0026sg-\u003elock);\n+\t\tseq = (u16)sg-\u003egen_seq_num;\n+\t\tif (++sg-\u003egen_seq_num \u003e= sg-\u003eseq_space)\n+\t\t\tsg-\u003egen_seq_num = 0;\n+\t\tsg-\u003estats_pkts++;\n+\t\tspin_unlock(\u0026sg-\u003elock);\n+\n+\t\tif (frer_rtag_push(skb, seq) \u003c 0) {\n+\t\t\ttcf_action_inc_drop_qstats(\u0026f-\u003ecommon);\n+\t\t\treturn TC_ACT_SHOT;\n+\t\t}\n+\t} else {\n+\t\tstruct frer_rcvy *rcvy = \u0026f-\u003ercvy;\n+\t\tbool pass;\n+\t\tint seq;\n+\n+\t\tif (frer_rtag_decode(skb, \u0026seq) \u003c 0) {\n+\t\t\ttcf_action_inc_drop_qstats(\u0026f-\u003ecommon);\n+\t\t\treturn TC_ACT_SHOT;\n+\t\t}\n+\n+\t\tspin_lock(\u0026rcvy-\u003elock);\n+\t\tif (rcvy-\u003ealg == TCA_FRER_RCVY_VECTOR_ALG)\n+\t\t\tpass = frer_vector_alg(rcvy, seq, f-\u003eindividual);\n+\t\telse\n+\t\t\tpass = frer_match_alg(rcvy, seq, f-\u003eindividual);\n+\n+\t\tif (pass) {\n+\t\t\trcvy-\u003estats_passed_pkts++;\n+\t\t\tspin_unlock(\u0026rcvy-\u003elock);\n+\t\t\tif (f-\u003etag_pop)\n+\t\t\t\tfrer_rtag_pop(skb);\n+\t\t\treturn retval;\n+\t\t}\n+\n+\t\trcvy-\u003estats_discarded_pkts++;\n+\t\tspin_unlock(\u0026rcvy-\u003elock);\n+\t\treturn TC_ACT_SHOT;\n+\t}\n+\n+\treturn retval;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Dump */\n+/* ------------------------------------------------------------------ */\n+\n+static int tcf_frer_dump(struct sk_buff *skb, struct tc_action *a,\n+\t\t\t int bind, int ref)\n+{\n+\tunsigned char *b = skb_tail_pointer(skb);\n+\tstruct tcf_frer *f = to_frer(a);\n+\tstruct tc_frer opt = {\n+\t\t.index = f-\u003etcf_index,\n+\t\t.refcnt = refcount_read(\u0026f-\u003etcf_refcnt) - ref,\n+\t\t.bindcnt = atomic_read(\u0026f-\u003etcf_bindcnt) - bind,\n+\t};\n+\tstruct tcf_t t;\n+\n+\tspin_lock_bh(\u0026f-\u003etcf_lock);\n+\topt.action = f-\u003etcf_action;\n+\n+\tif (nla_put(skb, TCA_FRER_PARMS, sizeof(opt), \u0026opt))\n+\t\tgoto nla_put_failure;\n+\tif (nla_put_u8(skb, TCA_FRER_FUNC, f-\u003efunc))\n+\t\tgoto nla_put_failure;\n+\tif (nla_put_u8(skb, TCA_FRER_TAG_TYPE, f-\u003etag_type))\n+\t\tgoto nla_put_failure;\n+\tif (f-\u003etag_pop \u0026\u0026 nla_put_flag(skb, TCA_FRER_RCVY_TAG_POP))\n+\t\tgoto nla_put_failure;\n+\n+\tif (f-\u003efunc == TCA_FRER_FUNC_PUSH) {\n+\t\tspin_lock(\u0026f-\u003eseqgen.lock);\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_SEQGEN_PKTS,\n+\t\t\t\t f-\u003eseqgen.stats_pkts, TCA_FRER_PAD)) {\n+\t\t\tspin_unlock(\u0026f-\u003eseqgen.lock);\n+\t\t\tgoto nla_put_failure;\n+\t\t}\n+\t\tspin_unlock(\u0026f-\u003eseqgen.lock);\n+\t} else {\n+\t\tu64 tagless, ooo, rogue, lost, resets, passed, discarded;\n+\t\tstruct frer_rcvy *rcvy = \u0026f-\u003ercvy;\n+\n+\t\tspin_lock(\u0026rcvy-\u003elock);\n+\t\ttagless = rcvy-\u003estats_tagless_pkts;\n+\t\tooo = rcvy-\u003estats_out_of_order_pkts;\n+\t\trogue = rcvy-\u003estats_rogue_pkts;\n+\t\tlost = rcvy-\u003estats_lost_pkts;\n+\t\tresets = rcvy-\u003estats_resets;\n+\t\tpassed = rcvy-\u003estats_passed_pkts;\n+\t\tdiscarded = rcvy-\u003estats_discarded_pkts;\n+\t\tspin_unlock(\u0026rcvy-\u003elock);\n+\n+\t\tif (f-\u003eindividual \u0026\u0026 nla_put_flag(skb, TCA_FRER_RCVY_INDIVIDUAL))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u8(skb, TCA_FRER_RCVY_ALG, rcvy-\u003ealg))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u8(skb, TCA_FRER_RCVY_HISTORY_LEN, rcvy-\u003ehistory_len))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u32(skb, TCA_FRER_RCVY_RESET_MSEC, rcvy-\u003ereset_msec))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (rcvy-\u003etake_no_seq \u0026\u0026 nla_put_flag(skb, TCA_FRER_RCVY_TAKE_NO_SEQ))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_TAGLESS_PKTS,\n+\t\t\t\t tagless, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_OUT_OF_ORDER_PKTS,\n+\t\t\t\t ooo, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_ROGUE_PKTS,\n+\t\t\t\t rogue, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_LOST_PKTS,\n+\t\t\t\t lost, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_RESETS,\n+\t\t\t\t resets, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_PASSED_PKTS,\n+\t\t\t\t passed, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t\tif (nla_put_u64_64bit(skb, TCA_FRER_STATS_DISCARDED_PKTS,\n+\t\t\t\t discarded, TCA_FRER_PAD))\n+\t\t\tgoto nla_put_failure;\n+\t}\n+\n+\ttcf_tm_dump(\u0026t, \u0026f-\u003etcf_tm);\n+\tif (nla_put_64bit(skb, TCA_FRER_TM, sizeof(t), \u0026t, TCA_FRER_PAD))\n+\t\tgoto nla_put_failure;\n+\n+\tspin_unlock_bh(\u0026f-\u003etcf_lock);\n+\treturn skb-\u003elen;\n+\n+nla_put_failure:\n+\tspin_unlock_bh(\u0026f-\u003etcf_lock);\n+\tnlmsg_trim(skb, b);\n+\treturn -1;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Cleanup */\n+/* ------------------------------------------------------------------ */\n+\n+static void tcf_frer_cleanup(struct tc_action *a)\n+{\n+\tstruct tcf_frer *f = to_frer(a);\n+\n+\tif (f-\u003efunc == TCA_FRER_FUNC_RECOVER)\n+\t\thrtimer_cancel(\u0026f-\u003ercvy.hrtimer);\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Walker / search / stats / fill-size / offload */\n+/* ------------------------------------------------------------------ */\n+\n+static int tcf_frer_walker(struct net *net, struct sk_buff *skb,\n+\t\t\t struct netlink_callback *cb, int type,\n+\t\t\t const struct tc_action_ops *ops,\n+\t\t\t struct netlink_ext_ack *extack)\n+{\n+\tstruct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);\n+\n+\treturn tcf_generic_walker(tn, skb, cb, type, ops, extack);\n+}\n+\n+static void tcf_frer_stats_update(struct tc_action *a, u64 bytes, u64 packets,\n+\t\t\t\t u64 drops, u64 lastuse, bool hw)\n+{\n+\tstruct tcf_frer *f = to_frer(a);\n+\tstruct tcf_t *tm = \u0026f-\u003etcf_tm;\n+\n+\ttcf_action_update_stats(a, bytes, packets, drops, hw);\n+\ttm-\u003elastuse = max_t(u64, tm-\u003elastuse, lastuse);\n+}\n+\n+static size_t tcf_frer_get_fill_size(const struct tc_action *act)\n+{\n+\treturn nla_total_size(sizeof(struct tc_frer)) /* TCA_FRER_PARMS */\n+\t\t+ nla_total_size(sizeof(u8)) /* TCA_FRER_FUNC */\n+\t\t+ nla_total_size(sizeof(u8)) /* TCA_FRER_TAG_TYPE */\n+\t\t+ nla_total_size(0) /* TCA_FRER_RCVY_TAG_POP (flag) */\n+\t\t+ nla_total_size(0) /* TCA_FRER_RCVY_INDIVIDUAL (flag) */\n+\t\t+ nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_ALG */\n+\t\t+ nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_HISTORY_LEN */\n+\t\t+ nla_total_size(sizeof(u32)) /* TCA_FRER_RCVY_RESET_MSEC */\n+\t\t+ nla_total_size(0) /* TCA_FRER_RCVY_TAKE_NO_SEQ (flag) */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_TAGLESS_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_OUT_OF_ORDER_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_ROGUE_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_LOST_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_RESETS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_PASSED_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_DISCARDED_PKTS */\n+\t\t+ nla_total_size_64bit(sizeof(struct tcf_t)); /* TCA_FRER_TM */\n+}\n+\n+static int tcf_frer_offload_act_setup(struct tc_action *act, void *entry_data,\n+\t\t\t\t u32 *index_inc, bool bind,\n+\t\t\t\t struct netlink_ext_ack *extack)\n+{\n+\tif (bind) {\n+\t\tstruct flow_action_entry *entry = entry_data;\n+\t\tstruct tcf_frer *f = to_frer(act);\n+\n+\t\tentry-\u003eid = FLOW_ACTION_FRER;\n+\t\tentry-\u003efrer.func = f-\u003efunc;\n+\t\tentry-\u003efrer.tag_type = f-\u003etag_type;\n+\t\tentry-\u003efrer.tag_pop = f-\u003etag_pop;\n+\n+\t\tif (f-\u003efunc != TCA_FRER_FUNC_PUSH) {\n+\t\t\tentry-\u003efrer.individual = f-\u003eindividual;\n+\t\t\tentry-\u003efrer.rcvy_alg = f-\u003ercvy.alg;\n+\t\t\tentry-\u003efrer.rcvy_history_len = f-\u003ercvy.history_len;\n+\t\t\tentry-\u003efrer.rcvy_reset_msec = f-\u003ercvy.reset_msec;\n+\t\t\tentry-\u003efrer.take_no_seq = f-\u003ercvy.take_no_seq;\n+\t\t}\n+\t\t*index_inc = 1;\n+\t} else {\n+\t\tstruct flow_offload_action *fl_action = entry_data;\n+\n+\t\tfl_action-\u003eid = FLOW_ACTION_FRER;\n+\t}\n+\treturn 0;\n+}\n+\n+/* ------------------------------------------------------------------ */\n+/* Module glue */\n+/* ------------------------------------------------------------------ */\n+\n+static struct tc_action_ops act_frer_ops = {\n+\t.kind\t\t = \"frer\",\n+\t.id\t\t = TCA_ID_FRER,\n+\t.owner\t\t = THIS_MODULE,\n+\t.act\t\t = tcf_frer_act,\n+\t.init\t\t = tcf_frer_init,\n+\t.cleanup\t = tcf_frer_cleanup,\n+\t.dump\t\t = tcf_frer_dump,\n+\t.walk\t\t = tcf_frer_walker,\n+\t.stats_update\t = tcf_frer_stats_update,\n+\t.get_fill_size\t = tcf_frer_get_fill_size,\n+\t.offload_act_setup = tcf_frer_offload_act_setup,\n+\t.size\t\t = sizeof(struct tcf_frer),\n+};\n+\n+static __net_init int frer_init_net(struct net *net)\n+{\n+\tstruct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);\n+\n+\treturn tc_action_net_init(net, tn, \u0026act_frer_ops);\n+}\n+\n+static void __net_exit frer_exit_net(struct list_head *net_list)\n+{\n+\ttc_action_net_exit(net_list, act_frer_ops.net_id);\n+}\n+\n+static struct pernet_operations frer_net_ops = {\n+\t.init = frer_init_net,\n+\t.exit_batch = frer_exit_net,\n+\t.id = \u0026act_frer_ops.net_id,\n+\t.size = sizeof(struct tc_action_net),\n+};\n+\n+static int __init frer_init_module(void)\n+{\n+\treturn tcf_register_action(\u0026act_frer_ops, \u0026frer_net_ops);\n+}\n+\n+static void __exit frer_cleanup_module(void)\n+{\n+\ttcf_unregister_action(\u0026act_frer_ops, \u0026frer_net_ops);\n+}\n+\n+module_init(frer_init_module);\n+module_exit(frer_cleanup_module);\n+MODULE_LICENSE(\"GPL\");\n+MODULE_DESCRIPTION(\"IEEE 802.1CB FRER tc action\");\ndiff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile\nindex 6a190a525a3917..67b896611f0865 100644\n--- a/tools/testing/selftests/net/Makefile\n+++ b/tools/testing/selftests/net/Makefile\n@@ -38,6 +38,7 @@ TEST_PROGS := \\\n \tfib_rule_tests.sh \\\n \tfib_tests.sh \\\n \tfin_ack_lat.sh \\\n+\tfrer_test.sh \\\n \tfq_band_pktlimit.sh \\\n \tgre_gso.sh \\\n \tgre_ipv6_lladdr.sh \\\ndiff --git a/tools/testing/selftests/net/frer_test.sh b/tools/testing/selftests/net/frer_test.sh\nnew file mode 100755\nindex 00000000000000..ecd88952f49581\n--- /dev/null\n+++ b/tools/testing/selftests/net/frer_test.sh\n@@ -0,0 +1,1013 @@\n+#!/bin/bash\n+# SPDX-License-Identifier: GPL-2.0\n+# Copyright 2026 NXP\n+#\n+# frer_test.sh - IEEE 802.1CB FRER tc action kselftest\n+#\n+# Topology for tests 1-4:\n+#\n+# ns_talker bond0 (veth_a0 + veth_b0) \u003c---\u003e bond1 (veth_a1 + veth_b1) ns_listener\n+#\n+# IP_SRC assigned to bond0; IP_DST assigned to bond1\n+#\n+# bond mode: balance-rr (round-robin), so frames are distributed across\n+# both slaves. FRER push is configured on both veth_a0 and\n+# veth_b0 egress with cross-mirror so every frame sent by either\n+# slave carries an R-TAG and a mirrored copy reaches the peer.\n+# FRER recover: veth_a1/veth_b1 ingress, shared or individual recover per test\n+#\n+# Ping runs from bond0 to bond1; tcpdump captures on bond1 (or on individual\n+# slave interfaces for tests where both copies must be observable).\n+#\n+# Test 5: simple point-to-point, self-contained topology (no bond).\n+# Test 6: relay system, self-contained topology.\n+#\n+# All namespaces, veth pairs, bond interfaces, tc rules and addresses are\n+# created and destroyed within this script. External dependencies:\n+# - kernel with CONFIG_NET_ACT_FRER and CONFIG_BONDING\n+# - iproute2 tc with frer action support\n+# - tcpdump, ping\n+# - root privileges\n+\n+# ----------------------------------------------------------------------------\n+# kselftest library: TAP output + exit-code constants\n+# ----------------------------------------------------------------------------\n+ksft_lib=\"${KSFT_LIB:-$(dirname \"$0\")/../kselftest/lib.sh}\"\n+if [ -f \"$ksft_lib\" ]; then\n+\t# shellcheck source=/dev/null\n+\t. \"$ksft_lib\"\n+else\n+\t# Minimal fallback when run outside the kselftest tree\n+\tKSFT_PASS=0\n+\tKSFT_FAIL=1\n+\tKSFT_SKIP=4\n+\t_ksft_count=0\n+\t_ksft_pass=0\n+\t_ksft_fail=0\n+\t_ksft_skip=0\n+\n+\tksft_print_header() { echo \"TAP version 13\"; }\n+\tksft_set_plan() { echo \"1..$1\"; }\n+\tksft_test_result_pass() {\n+\t\t_ksft_count=$((_ksft_count + 1)); _ksft_pass=$((_ksft_pass + 1))\n+\t\techo \"ok $_ksft_count - $*\"\n+\t}\n+\tksft_test_result_fail() {\n+\t\t_ksft_count=$((_ksft_count + 1)); _ksft_fail=$((_ksft_fail + 1))\n+\t\techo \"not ok $_ksft_count - $*\"\n+\t}\n+\tksft_test_result_skip() {\n+\t\t_ksft_count=$((_ksft_count + 1)); _ksft_skip=$((_ksft_skip + 1))\n+\t\techo \"ok $_ksft_count - $* # SKIP\"\n+\t}\n+\tksft_print_cnts() {\n+\t\techo \"# Totals: pass=$_ksft_pass fail=$_ksft_fail skip=$_ksft_skip\"\n+\t}\n+\tksft_exit_pass() { exit $KSFT_PASS; }\n+\tksft_exit_fail() { exit $KSFT_FAIL; }\n+\tksft_exit_fail_msg() { echo \"# FATAL: $*\" \u003e\u00262; exit $KSFT_FAIL; }\n+fi\n+\n+# ----------------------------------------------------------------------------\n+# Configuration (override via environment)\n+# ----------------------------------------------------------------------------\n+TC=\"${TC:-tc}\"\n+PING=\"${PING:-ping}\"\n+TCPDUMP=\"${TCPDUMP:-tcpdump}\"\n+PING_COUNT=\"${PING_COUNT:-5}\"\n+PING_TIMEOUT=\"${PING_TIMEOUT:-2}\"\n+SKIP_MODPROBE=\"${SKIP_MODPROBE:-0}\"\n+\n+# Bond topology interfaces (tests 1-4)\n+readonly VETH_A0=\"frer_a0\"\n+readonly VETH_A1=\"frer_a1\"\n+readonly VETH_B0=\"frer_b0\"\n+readonly VETH_B1=\"frer_b1\"\n+readonly BOND0=\"frer_bond0\"\n+readonly BOND1=\"frer_bond1\"\n+\n+readonly NS_TALKER=\"frer_ns_talker\"\n+readonly NS_LISTENER=\"frer_ns_listener\"\n+\n+readonly IP_SRC=\"10.0.0.1\"\n+readonly IP_DST=\"10.0.0.2\"\n+\n+# Point-to-point topology interfaces (test 5)\n+readonly P2P_NS_SRC=\"frer_p2p_src\"\n+readonly P2P_NS_DST=\"frer_p2p_dst\"\n+readonly P2P_VETH_A0=\"frer_p2p_a0\"\n+readonly P2P_VETH_A1=\"frer_p2p_a1\"\n+readonly IP_P2P_SRC=\"10.0.1.1\"\n+readonly IP_P2P_DST=\"10.0.1.2\"\n+\n+# Relay topology interfaces (test 6)\n+#\n+# ns_talker (talker_eth.100) -- talker_eth/br0_uplink -- bridge0 (br_r0)\n+# |-- br0_swp0/br1_swp0 --\\\n+# \\-- br0_swp1/br1_swp1 --+--\\\n+#\t\tbridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener\n+#\n+# bridge0 acts as sequence generator (frer push + replicate to both paths).\n+# bridge1 acts as eliminator (frer recover, shared, tag-pop).\n+readonly R_NS_TALKER=\"frer_r_talker\"\n+readonly R_NS_BRIDGE0=\"frer_r_bridge0\"\n+readonly R_NS_BRIDGE1=\"frer_r_bridge1\"\n+readonly R_NS_LISTENER=\"frer_r_listener\"\n+readonly R_TALKER_ETH=\"r_tlk_eth\" # talker-side physical port\n+readonly R_BR0_UPLINK=\"r_br0_uplink\" # bridge0 uplink facing talker\n+readonly R_BR0_SWP0=\"r_br0_swp0\" # bridge0 redundant path port 0\n+readonly R_BR0_SWP1=\"r_br0_swp1\" # bridge0 redundant path port 1\n+readonly R_BR1_SWP0=\"r_br1_swp0\" # bridge1 redundant path port 0\n+readonly R_BR1_SWP1=\"r_br1_swp1\" # bridge1 redundant path port 1\n+readonly R_BR1_DOWNLINK=\"r_br1_dwnlnk\" # bridge1 downlink facing listener\n+readonly R_LISTENER_ETH=\"r_lst_eth\" # listener-side physical port\n+readonly R_BR0=\"br_r0\"\n+readonly R_BR1=\"br_r1\"\n+readonly R_VLAN=100\n+readonly R_IP_TALKER=\"10.1.0.1\"\n+readonly R_IP_LISTENER=\"10.1.0.2\"\n+\n+# FRER action index constants\n+readonly IDX_PUSH=1\n+readonly IDX_SHARED_RCVY=10\n+readonly IDX_INDV_RCVY_A=20\n+readonly IDX_INDV_RCVY_B=21\n+readonly IDX_NO_POP=30\n+readonly IDX_P2P_RCVY=40\n+readonly IDX_RELAY_PUSH=50\n+readonly IDX_RELAY_RCVY=60\n+\n+readonly NUM_TESTS=6\n+\n+# ----------------------------------------------------------------------------\n+# Prerequisite check\n+# ----------------------------------------------------------------------------\n+check_prerequisites()\n+{\n+\tlocal missing=0\n+\n+\t[ \"$(id -u)\" -eq 0 ] || { echo \"# Must be run as root\" \u003e\u00262; missing=1; }\n+\n+\tfor cmd in ip \"$TC\" \"$TCPDUMP\" \"$PING\"; do\n+\t\tcommand -v \"$cmd\" \u003e/dev/null 2\u003e\u00261 || {\n+\t\t\techo \"# Missing command: $cmd\" \u003e\u00262\n+\t\t\tmissing=1\n+\t\t}\n+\tdone\n+\n+\tif [ \"$missing\" -ne 0 ]; then\n+\t\tksft_set_plan \"$NUM_TESTS\"\n+\t\tfor i in $(seq 1 \"$NUM_TESTS\"); do\n+\t\t\tksft_test_result_skip \"prerequisites not met (test $i)\"\n+\t\tdone\n+\t\tksft_print_cnts\n+\t\texit \"$KSFT_SKIP\"\n+\tfi\n+}\n+\n+load_module()\n+{\n+\t[ \"$SKIP_MODPROBE\" = \"1\" ] \u0026\u0026 return\n+\tif ! modprobe act_frer 2\u003e/dev/null; then\n+\t\techo \"# modprobe act_frer failed - may be built-in or unavailable\" \u003e\u00262\n+\tfi\n+\tif ! modprobe bonding 2\u003e/dev/null; then\n+\t\techo \"# modprobe bonding failed - may be built-in or unavailable\" \u003e\u00262\n+\tfi\n+}\n+\n+check_frer_action()\n+{\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$TC actions add action frer push index 999 2\u003e/dev/null || return 1\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$TC actions del action frer index 999 2\u003e/dev/null || true\n+\treturn 0\n+}\n+\n+# ----------------------------------------------------------------------------\n+# Bond topology setup / teardown (used by tests 1-4)\n+# ----------------------------------------------------------------------------\n+setup_topology()\n+{\n+\tfor n in \"$NS_TALKER\" \"$NS_LISTENER\"; do\n+\t\tip netns add \"$n\"\n+\tdone\n+\n+\tip link add \"$VETH_A0\" type veth peer name \"$VETH_A1\"\n+\tip link set \"$VETH_A0\" netns \"$NS_TALKER\"\n+\tip link set \"$VETH_A1\" netns \"$NS_LISTENER\"\n+\n+\tip link add \"$VETH_B0\" type veth peer name \"$VETH_B1\"\n+\tip link set \"$VETH_B0\" netns \"$NS_TALKER\"\n+\tip link set \"$VETH_B1\" netns \"$NS_LISTENER\"\n+\n+\t# ns_talker: create bond0 (balance-rr), frames round-robin across both slaves.\n+\tip netns exec \"$NS_TALKER\" ip link set lo up\n+\tip netns exec \"$NS_TALKER\" ip link add \"$BOND0\" type bond mode balance-rr miimon 100\n+\tip netns exec \"$NS_TALKER\" ip link set \"$VETH_A0\" master \"$BOND0\"\n+\tip netns exec \"$NS_TALKER\" ip link set \"$VETH_B0\" master \"$BOND0\"\n+\tip netns exec \"$NS_TALKER\" ip link set \"$VETH_A0\" up\n+\tip netns exec \"$NS_TALKER\" ip link set \"$VETH_B0\" up\n+\tip netns exec \"$NS_TALKER\" ip link set \"$BOND0\" up\n+\tip netns exec \"$NS_TALKER\" ip addr add \"${IP_SRC}/24\" dev \"$BOND0\"\n+\n+\t# ns_listener: create bond1 (balance-rr).\n+\tip netns exec \"$NS_LISTENER\" ip link set lo up\n+\tip netns exec \"$NS_LISTENER\" ip link add \"$BOND1\" type bond mode balance-rr miimon 100\n+\tip netns exec \"$NS_LISTENER\" ip link set \"$VETH_A1\" master \"$BOND1\"\n+\tip netns exec \"$NS_LISTENER\" ip link set \"$VETH_B1\" master \"$BOND1\"\n+\tip netns exec \"$NS_LISTENER\" ip link set \"$VETH_A1\" up\n+\tip netns exec \"$NS_LISTENER\" ip link set \"$VETH_B1\" up\n+\tip netns exec \"$NS_LISTENER\" ip link set \"$BOND1\" up\n+\tip netns exec \"$NS_LISTENER\" ip addr add \"${IP_DST}/24\" dev \"$BOND1\"\n+\n+\t# Static ARP so L2 forwarding works without ARP broadcasts.\n+\t# With balance-rr both slaves share the bond MAC.\n+\tlocal mac_bond0 mac_bond1\n+\tmac_bond0=$(ip netns exec \"$NS_TALKER\" cat /sys/class/net/\"$BOND0\"/address)\n+\tmac_bond1=$(ip netns exec \"$NS_LISTENER\" cat /sys/class/net/\"$BOND1\"/address)\n+\tip netns exec \"$NS_TALKER\" ip neigh add \"$IP_DST\" lladdr \"$mac_bond1\" dev \"$BOND0\"\n+\tip netns exec \"$NS_LISTENER\" ip neigh add \"$IP_SRC\" lladdr \"$mac_bond0\" dev \"$BOND1\"\n+}\n+\n+cleanup()\n+{\n+\tfor n in \"$NS_TALKER\" \"$NS_LISTENER\" \\\n+\t\t\"$P2P_NS_SRC\" \"$P2P_NS_DST\" \\\n+\t\t\"$R_NS_TALKER\" \"$R_NS_BRIDGE0\" \"$R_NS_BRIDGE1\" \"$R_NS_LISTENER\"; do\n+\t\tip netns del \"$n\" 2\u003e/dev/null || true\n+\tdone\n+}\n+trap cleanup EXIT\n+\n+# ----------------------------------------------------------------------------\n+# TC rule helpers\n+# ----------------------------------------------------------------------------\n+\n+# Push on both veth_a0 and veth_b0 egress using the same shared frer push\n+# action (IDX_PUSH). Each slave also mirrors to the other so that every\n+# outgoing frame is replicated onto both paths regardless of which slave the\n+# bond currently selects. This prevents packet loss during bond link changes.\n+setup_push_mirror()\n+{\n+\tip netns exec \"$NS_TALKER\" $TC qdisc add dev \"$VETH_A0\" clsact\n+\tip netns exec \"$NS_TALKER\" $TC filter add dev \"$VETH_A0\" egress \\\n+\t\tprotocol ip flower skip_hw \\\n+\t\taction frer push index $IDX_PUSH \\\n+\t\taction mirred egress mirror dev \"$VETH_B0\"\n+\n+\tip netns exec \"$NS_TALKER\" $TC qdisc add dev \"$VETH_B0\" clsact\n+\tip netns exec \"$NS_TALKER\" $TC filter add dev \"$VETH_B0\" egress \\\n+\t\tprotocol ip flower skip_hw \\\n+\t\taction frer push index $IDX_PUSH \\\n+\t\taction mirred egress mirror dev \"$VETH_A0\"\n+}\n+\n+teardown_tc()\n+{\n+\tfor dev in \"$VETH_A0\" \"$VETH_B0\"; do\n+\t\tip netns exec \"$NS_TALKER\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tfor dev in \"$VETH_A1\" \"$VETH_B1\"; do\n+\t\tip netns exec \"$NS_LISTENER\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tip netns exec \"$NS_TALKER\" $TC actions flush action frer 2\u003e/dev/null || true\n+\tip netns exec \"$NS_LISTENER\" $TC actions flush action frer 2\u003e/dev/null || true\n+}\n+\n+# ----------------------------------------------------------------------------\n+# Packet-capture helpers\n+#\n+# capture_start_on NS IFACE PCAP [BPF_FILTER]\n+# Starts tcpdump in namespace NS on IFACE, writing to PCAP.\n+# Stores PID in _CAP_PID.\n+#\n+# capture_stop\n+# Waits for tcpdump (stored in _CAP_PID) to finish.\n+#\n+# capture_count_on NS PCAP\n+# Prints the number of captured packets.\n+#\n+# Convenience wrappers capture_start / capture_count target bond1 in\n+# NS_LISTENER (the primary observation point for tests 2 and 4).\n+# ----------------------------------------------------------------------------\n+_CAP_PID=\"\"\n+\n+capture_start_on()\n+{\n+\tlocal ns=\"$1\" iface=\"$2\" pcap=\"$3\" filter=\"${4:-}\"\n+\n+\tif [ -n \"$filter\" ]; then\n+\t\tip netns exec \"$ns\" timeout 4 \\\n+\t\t\t$TCPDUMP -i \"$iface\" -w \"$pcap\" \\\n+\t\t\t--immediate-mode -Z root -y EN10MB \\\n+\t\t\t$filter \u003e/dev/null 2\u003e\u00261 \u0026\n+\telse\n+\t\tip netns exec \"$ns\" timeout 4 \\\n+\t\t\t$TCPDUMP -i \"$iface\" -w \"$pcap\" \\\n+\t\t\t--immediate-mode -Z root -y EN10MB \\\n+\t\t\t\u003e/dev/null 2\u003e\u00261 \u0026\n+\tfi\n+\t_CAP_PID=$!\n+\n+\t# Wait until tcpdump opens a packet socket (max ~2.5 s).\n+\tlocal tries=0\n+\twhile [ $tries -lt 50 ]; do\n+\t\tip netns exec \"$ns\" grep -q \"$iface\" /proc/net/packet 2\u003e/dev/null \u0026\u0026 break\n+\t\tsleep 0.05\n+\t\ttries=$((tries + 1))\n+\tdone\n+}\n+\n+capture_stop()\n+{\n+\t[ -n \"$_CAP_PID\" ] || return 0\n+\twait \"$_CAP_PID\" 2\u003e/dev/null || true\n+\t_CAP_PID=\"\"\n+}\n+\n+capture_count_on()\n+{\n+\tlocal ns=\"$1\" pcap=\"$2\"\n+\tip netns exec \"$ns\" \\\n+\t\t$TCPDUMP -r \"$pcap\" --no-promiscuous-mode 2\u003e/dev/null \\\n+\t\t| grep -c \"^[0-9]\" || true\n+}\n+\n+# Convenience wrappers: default to bond1 in NS_LISTENER\n+capture_start() { capture_start_on \"$NS_LISTENER\" \"$BOND1\" \"$@\"; }\n+capture_count() { capture_count_on \"$NS_LISTENER\" \"$1\"; }\n+\n+# ----------------------------------------------------------------------------\n+# Ping helper\n+# ----------------------------------------------------------------------------\n+do_ping()\n+{\n+\tlocal rc=0\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$PING -c \"$PING_COUNT\" -W \"$PING_TIMEOUT\" -i 0.2 -q \\\n+\t\t\"$IP_DST\" \u003e/dev/null 2\u003e\u00261 || rc=$?\n+\treturn $rc\n+}\n+\n+# ----------------------------------------------------------------------------\n+# tc statistics parser\n+# ----------------------------------------------------------------------------\n+tc_stat()\n+{\n+\tlocal dump=\"$1\" field=\"$2\"\n+\techo \"$dump\" | awk -F\"${field}=\" 'NF\u003e1{split($2,a,\" \");print a[1];exit}' || echo \"0\"\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 1: PUSH VERIFY (bond topology)\n+#\n+# Only push is configured on the talker side; no recover on the listener.\n+# The push action on veth_a0 egress inserts an R-TAG and mirrors a copy to\n+# veth_b0, so both listener slaves (veth_a1 and veth_b1) receive a frame\n+# with EtherType 0xF1C1. Captures run sequentially on each slave to verify\n+# that both paths carry R-TAG frames.\n+#\n+# Pass criteria:\n+# - veth_a1 captures \u003e= 1 R-TAG frame\n+# - veth_b1 captures \u003e= 1 R-TAG frame\n+# ----------------------------------------------------------------------------\n+test_push_verify_bond()\n+{\n+\tlocal pcap_a pcap_b cap_a cap_b\n+\tlocal result=\"pass\"\n+\n+\tsetup_push_mirror\n+\n+\t# Capture 1: R-TAG frames on veth_a1 (path A)\n+\tpcap_a=$(mktemp /tmp/frer_bond_push_a_XXXXXX.pcap)\n+\tcapture_start_on \"$NS_LISTENER\" \"$VETH_A1\" \"$pcap_a\" \"ether proto 0xf1c1\"\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$PING -c 3 -W 1 -i 0.2 -q \"$IP_DST\" \u003e/dev/null 2\u003e\u00261 || true\n+\tcapture_stop\n+\tcap_a=$(capture_count_on \"$NS_LISTENER\" \"$pcap_a\")\n+\trm -f \"$pcap_a\"\n+\n+\t# Capture 2: R-TAG frames on veth_b1 (path B, mirrored copy)\n+\tpcap_b=$(mktemp /tmp/frer_bond_push_b_XXXXXX.pcap)\n+\tcapture_start_on \"$NS_LISTENER\" \"$VETH_B1\" \"$pcap_b\" \"ether proto 0xf1c1\"\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$PING -c 3 -W 1 -i 0.2 -q \"$IP_DST\" \u003e/dev/null 2\u003e\u00261 || true\n+\tcapture_stop\n+\tcap_b=$(capture_count_on \"$NS_LISTENER\" \"$pcap_b\")\n+\trm -f \"$pcap_b\"\n+\n+\tteardown_tc\n+\n+\techo \"# bond push verify: veth_a1 R-TAG=$cap_a veth_b1 R-TAG=$cap_b\"\n+\n+\t[ \"$cap_a\" -ge 1 ] || result=\"fail\"\n+\t[ \"$cap_b\" -ge 1 ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"bond push verify: R-TAG on both paths (a1=$cap_a b1=$cap_b)\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"bond push verify: expected R-TAG on both paths (a1=$cap_a b1=$cap_b)\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 2: SHARED RECOVER E2E (bond topology)\n+#\n+# veth_a1 and veth_b1 ingress share one recover action (idx=10) with tag-pop.\n+# The listener receives two R-TAG copies per request; the shared recover passes\n+# exactly one and discards the other. The recovered plain ICMP reaches bond1's\n+# IP stack and a reply is sent, making ping succeed.\n+#\n+# Pass criteria:\n+# - ping succeeds (rc=0)\n+# - tcpdump on bond1 captures exactly PING_COUNT ICMP echo-request frames\n+# (filter is restricted to type=8 to exclude echo replies, which would\n+# double the count since bond1 also originates the reply packets)\n+# - tc stats on veth_a1: passed \u003e= PING_COUNT, discarded \u003e= PING_COUNT\n+# ----------------------------------------------------------------------------\n+test_shared_recover_bond()\n+{\n+\tlocal pcap cap_count ping_rc=0\n+\tlocal dump_a\n+\tlocal total_passed total_discarded tagless\n+\tlocal result=\"pass\"\n+\n+\tsetup_push_mirror\n+\n+\t# veth_a1 ingress: create shared recover action with tag-pop\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_A1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_A1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover alg vector history-length 16 \\\n+\t\t\treset-time 2000 tag-pop index $IDX_SHARED_RCVY\n+\n+\t# veth_b1 ingress: bind to the same shared action by index\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_B1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_B1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover index $IDX_SHARED_RCVY\n+\n+\tpcap=$(mktemp /tmp/frer_bond_shared_XXXXXX.pcap)\n+\tcapture_start \"$pcap\" \"icmp[icmptype] == icmp-echo\"\n+\n+\tdo_ping || ping_rc=$?\n+\n+\tcapture_stop\n+\n+\tcap_count=$(capture_count \"$pcap\")\n+\trm -f \"$pcap\"\n+\n+\tdump_a=$(ip netns exec \"$NS_LISTENER\" \\\n+\t\t$TC -s filter show dev \"$VETH_A1\" ingress 2\u003e/dev/null)\n+\n+\tteardown_tc\n+\n+\ttotal_passed=$(tc_stat \"$dump_a\" \"passed\")\n+\ttotal_discarded=$(tc_stat \"$dump_a\" \"discarded\")\n+\ttagless=$(tc_stat \"$dump_a\" \"tagless\")\n+\ttotal_discarded=$((total_discarded - tagless))\n+\n+\techo \"# bond shared recover: ping_rc=$ping_rc cap=$cap_count\" \\\n+\t\t\"passed=$total_passed discarded=$total_discarded\"\n+\n+\t[ \"$ping_rc\" -eq 0 ] || result=\"fail\"\n+\t[ \"$cap_count\" -eq \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$total_passed\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$total_discarded\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"bond shared recover: ping OK, cap=$cap_count\" \\\n+\t\t\t\"passed=$total_passed discarded=$total_discarded\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"bond shared recover: ping_rc=$ping_rc cap=$cap_count\" \\\n+\t\t\t\"passed=$total_passed discarded=$total_discarded\" \\\n+\t\t\t\"(expected ping OK, cap=$PING_COUNT,\" \\\n+\t\t\t\"passed\u003e=$PING_COUNT, discarded\u003e=$PING_COUNT)\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 3: INDIVIDUAL RECOVER (bond topology)\n+#\n+# veth_a1 and veth_b1 use independent recover actions (idx=20 and idx=21).\n+# Each port maintains its own sequence history so both copies of every frame\n+# are passed (no cross-port deduplication). With active-backup bond1, only\n+# the active slave's (veth_a1) recovered frame reaches bond1's IP stack, so\n+# ping succeeds. The absence of deduplication is verified via per-slave\n+# tcpdump (each slave should capture PING_COUNT ICMP frames) and tc stats.\n+#\n+# Pass criteria:\n+# - ping succeeds\n+# - veth_a1 captures PING_COUNT ICMP frames (passed, not discarded)\n+# - veth_b1 captures PING_COUNT ICMP frames (passed independently)\n+# - tc stats: veth_a1 passed=PING_COUNT discarded=0\n+# veth_b1 passed=PING_COUNT discarded=0\n+# ----------------------------------------------------------------------------\n+test_individual_recover_bond()\n+{\n+\tlocal pcap_a pcap_b cap_a cap_b ping_rc=0\n+\tlocal dump_a dump_b\n+\tlocal passed_a discarded_a passed_b discarded_b tagless_a tagless_b\n+\tlocal result=\"pass\"\n+\n+\tsetup_push_mirror\n+\n+\t# veth_a1 ingress: individual recover idx=20 (independent state)\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_A1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_A1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover individual alg vector history-length 16 \\\n+\t\t\treset-time 2000 tag-pop index $IDX_INDV_RCVY_A\n+\n+\t# veth_b1 ingress: individual recover idx=21 (separate independent state)\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_B1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_B1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover individual alg vector history-length 16 \\\n+\t\t\treset-time 2000 tag-pop index $IDX_INDV_RCVY_B\n+\n+\t# Per-slave capture A: verify veth_a1 passes frames; also use this run\n+\t# for the overall ping_rc check (do_ping targets bond0-\u003ebond1).\n+\tpcap_a=$(mktemp /tmp/frer_bond_indv_a_XXXXXX.pcap)\n+\tcapture_start_on \"$NS_LISTENER\" \"$VETH_A1\" \"$pcap_a\" \"icmp\"\n+\tdo_ping || ping_rc=$?\n+\tcapture_stop\n+\tcap_a=$(capture_count_on \"$NS_LISTENER\" \"$pcap_a\")\n+\trm -f \"$pcap_a\"\n+\n+\t# Per-slave capture B: verify veth_b1 also passes frames (balance-rr\n+\t# distributes egress across both slaves, so both paths carry traffic).\n+\tpcap_b=$(mktemp /tmp/frer_bond_indv_b_XXXXXX.pcap)\n+\tcapture_start_on \"$NS_LISTENER\" \"$VETH_B1\" \"$pcap_b\" \"icmp\"\n+\tdo_ping || true\n+\tcapture_stop\n+\tcap_b=$(capture_count_on \"$NS_LISTENER\" \"$pcap_b\")\n+\trm -f \"$pcap_b\"\n+\n+\tdump_a=$(ip netns exec \"$NS_LISTENER\" \\\n+\t\t$TC -s filter show dev \"$VETH_A1\" ingress 2\u003e/dev/null)\n+\tdump_b=$(ip netns exec \"$NS_LISTENER\" \\\n+\t\t$TC -s filter show dev \"$VETH_B1\" ingress 2\u003e/dev/null)\n+\n+\tteardown_tc\n+\n+\tpassed_a=$(tc_stat \"$dump_a\" \"passed\")\n+\tdiscarded_a=$(tc_stat \"$dump_a\" \"discarded\")\n+\ttagless_a=$(tc_stat \"$dump_a\" \"tagless\")\n+\tpassed_b=$(tc_stat \"$dump_b\" \"passed\")\n+\tdiscarded_b=$(tc_stat \"$dump_b\" \"discarded\")\n+\ttagless_b=$(tc_stat \"$dump_b\" \"tagless\")\n+\tdiscarded_a=$((discarded_a - tagless_a))\n+\tdiscarded_b=$((discarded_b - tagless_b))\n+\n+\techo \"# bond individual recover: ping_rc=$ping_rc\" \\\n+\t\t\"a1: cap=$cap_a passed=$passed_a discarded=$discarded_a\" \\\n+\t\t\"b1: cap=$cap_b passed=$passed_b discarded=$discarded_b\"\n+\n+\t[ \"$ping_rc\" -eq 0 ] || result=\"fail\"\n+\t[ \"$cap_a\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$cap_b\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$passed_a\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$passed_b\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$discarded_a\" -eq 0 ] || result=\"fail\"\n+\t[ \"$discarded_b\" -eq 0 ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"bond individual recover: ping OK\" \\\n+\t\t\t\"a1: cap=$cap_a passed=$passed_a/0\" \\\n+\t\t\t\"b1: cap=$cap_b passed=$passed_b/0\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"bond individual recover: ping_rc=$ping_rc\" \\\n+\t\t\t\"a1: cap=$cap_a passed=$passed_a discarded=$discarded_a\" \\\n+\t\t\t\"b1: cap=$cap_b passed=$passed_b discarded=$discarded_b\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 4: NO TAG-POP (bond topology)\n+#\n+# Shared recover runs without tag-pop; passed frames still carry the R-TAG\n+# when they reach bond1.\n+#\n+# Pass criteria:\n+# - tcpdump on bond1 with \"ether proto 0xf1c1\" captures \u003e= 1 R-TAG frame\n+# - tcpdump on bond1 with \"icmp\" captures 0 frames (outer EtherType is\n+# 0xF1C1, not 0x0800, so plain-IP ICMP filter does not match)\n+# ----------------------------------------------------------------------------\n+test_no_tag_pop_bond()\n+{\n+\tlocal pcap_rtag pcap_icmp rtag_count icmp_count\n+\tlocal result=\"pass\"\n+\n+\tsetup_push_mirror\n+\n+\t# veth_a1 ingress: shared recover WITHOUT tag-pop\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_A1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_A1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover alg vector history-length 16 \\\n+\t\t\treset-time 2000 index $IDX_NO_POP\n+\n+\t# veth_b1 ingress: bind to the same shared action\n+\tip netns exec \"$NS_LISTENER\" $TC qdisc add dev \"$VETH_B1\" clsact\n+\tip netns exec \"$NS_LISTENER\" $TC filter add dev \"$VETH_B1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover index $IDX_NO_POP\n+\n+\t# Capture 1: frames with R-TAG EtherType on bond1 (expect \u003e= 1)\n+\tpcap_rtag=$(mktemp /tmp/frer_bond_nopop_rtag_XXXXXX.pcap)\n+\tcapture_start \"$pcap_rtag\" \"ether proto 0xf1c1\"\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$PING -c 3 -W 1 -i 0.2 -q \"$IP_DST\" \u003e/dev/null 2\u003e\u00261 || true\n+\tcapture_stop\n+\trtag_count=$(capture_count \"$pcap_rtag\")\n+\trm -f \"$pcap_rtag\"\n+\n+\t# Capture 2: plain ICMP frames on bond1 (expect 0)\n+\tpcap_icmp=$(mktemp /tmp/frer_bond_nopop_icmp_XXXXXX.pcap)\n+\tcapture_start \"$pcap_icmp\" \"icmp\"\n+\tip netns exec \"$NS_TALKER\" \\\n+\t\t$PING -c 3 -W 1 -i 0.2 -q \"$IP_DST\" \u003e/dev/null 2\u003e\u00261 || true\n+\tcapture_stop\n+\ticmp_count=$(capture_count \"$pcap_icmp\")\n+\trm -f \"$pcap_icmp\"\n+\n+\tteardown_tc\n+\n+\techo \"# bond no tag-pop: rtag=$rtag_count (expected \u003e=1) icmp=$icmp_count (expected 0)\"\n+\n+\t[ \"$rtag_count\" -ge 1 ] || result=\"fail\"\n+\t[ \"$icmp_count\" -eq 0 ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"bond no tag-pop: R-TAG present on bond1 \" \\\n+\t\t\t\"(rtag=$rtag_count), ICMP absent (icmp=$icmp_count)\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"bond no tag-pop: rtag=$rtag_count icmp=$icmp_count \" \\\n+\t\t\t\"(expected rtag\u003e=1 icmp=0)\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 5: SIMPLE POINT-TO-POINT (no bond)\n+#\n+# Self-contained single-path topology: push on p2p_a0 egress, individual\n+# recover (with tag-pop) on p2p_a1 ingress. IP is assigned directly to the\n+# veth interfaces (no bond).\n+#\n+# Pass criteria:\n+# - ping succeeds (rc=0)\n+# - veth_a1 recover stats: passed \u003e= PING_COUNT, discarded = 0\n+# ----------------------------------------------------------------------------\n+test_simple_point_to_point()\n+{\n+\tlocal ping_rc=0\n+\tlocal dump_a1 passed discarded\n+\tlocal result=\"pass\"\n+\n+\t# Create self-contained p2p namespaces\n+\tip netns add \"$P2P_NS_SRC\"\n+\tip netns add \"$P2P_NS_DST\"\n+\n+\tip link add \"$P2P_VETH_A0\" type veth peer name \"$P2P_VETH_A1\"\n+\tip link set \"$P2P_VETH_A0\" netns \"$P2P_NS_SRC\"\n+\tip link set \"$P2P_VETH_A1\" netns \"$P2P_NS_DST\"\n+\n+\tip netns exec \"$P2P_NS_SRC\" ip link set lo up\n+\tip netns exec \"$P2P_NS_SRC\" ip link set \"$P2P_VETH_A0\" up\n+\tip netns exec \"$P2P_NS_SRC\" ip addr add \"${IP_P2P_SRC}/24\" dev \"$P2P_VETH_A0\"\n+\n+\tip netns exec \"$P2P_NS_DST\" ip link set lo up\n+\tip netns exec \"$P2P_NS_DST\" ip link set \"$P2P_VETH_A1\" up\n+\tip netns exec \"$P2P_NS_DST\" ip addr add \"${IP_P2P_DST}/24\" dev \"$P2P_VETH_A1\"\n+\n+\tlocal mac_a0 mac_a1\n+\tmac_a0=$(ip netns exec \"$P2P_NS_SRC\" cat /sys/class/net/\"$P2P_VETH_A0\"/address)\n+\tmac_a1=$(ip netns exec \"$P2P_NS_DST\" cat /sys/class/net/\"$P2P_VETH_A1\"/address)\n+\tip netns exec \"$P2P_NS_SRC\" ip neigh add \"$IP_P2P_DST\" lladdr \"$mac_a1\" dev \"$P2P_VETH_A0\"\n+\tip netns exec \"$P2P_NS_DST\" ip neigh add \"$IP_P2P_SRC\" lladdr \"$mac_a0\" dev \"$P2P_VETH_A1\"\n+\n+\t# veth_a0 egress: push R-TAG\n+\tip netns exec \"$P2P_NS_SRC\" $TC qdisc add dev \"$P2P_VETH_A0\" clsact\n+\tip netns exec \"$P2P_NS_SRC\" $TC filter add dev \"$P2P_VETH_A0\" egress \\\n+\t\tprotocol ip flower skip_hw \\\n+\t\taction frer push index $IDX_PUSH\n+\n+\t# veth_a1 ingress: individual recover with tag-pop\n+\tip netns exec \"$P2P_NS_DST\" $TC qdisc add dev \"$P2P_VETH_A1\" clsact\n+\tip netns exec \"$P2P_NS_DST\" $TC filter add dev \"$P2P_VETH_A1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover individual alg vector history-length 16 \\\n+\t\t\treset-time 2000 tag-pop index $IDX_P2P_RCVY\n+\n+\tip netns exec \"$P2P_NS_SRC\" \\\n+\t\t$PING -c \"$PING_COUNT\" -W \"$PING_TIMEOUT\" -i 0.2 -q \\\n+\t\t\"$IP_P2P_DST\" \u003e/dev/null 2\u003e\u00261 || ping_rc=$?\n+\n+\tdump_a1=$(ip netns exec \"$P2P_NS_DST\" \\\n+\t\t$TC -s filter show dev \"$P2P_VETH_A1\" ingress 2\u003e/dev/null)\n+\n+\t# Teardown p2p topology\n+\tfor dev in \"$P2P_VETH_A0\"; do\n+\t\tip netns exec \"$P2P_NS_SRC\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tfor dev in \"$P2P_VETH_A1\"; do\n+\t\tip netns exec \"$P2P_NS_DST\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tip netns exec \"$P2P_NS_SRC\" $TC actions flush action frer 2\u003e/dev/null || true\n+\tip netns exec \"$P2P_NS_DST\" $TC actions flush action frer 2\u003e/dev/null || true\n+\tip netns del \"$P2P_NS_SRC\" 2\u003e/dev/null || true\n+\tip netns del \"$P2P_NS_DST\" 2\u003e/dev/null || true\n+\n+\tpassed=$(tc_stat \"$dump_a1\" \"passed\")\n+\tdiscarded=$(tc_stat \"$dump_a1\" \"discarded\")\n+\tlocal tagless\n+\ttagless=$(tc_stat \"$dump_a1\" \"tagless\")\n+\tdiscarded=$((discarded - tagless))\n+\n+\techo \"# p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded\"\n+\n+\t[ \"$ping_rc\" -eq 0 ] || result=\"fail\"\n+\t[ \"$passed\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$discarded\" -eq 0 ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"simple p2p: ping OK, passed=$passed discarded=$discarded\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"simple p2p: ping_rc=$ping_rc passed=$passed discarded=$discarded\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# TEST 6: RELAY E2E (self-contained, no bond)\n+#\n+# Talker sends VLAN-100 frames into bridge0 (sequence generator). Bridge0\n+# pushes an R-TAG and replicates to two redundant paths. Bridge1 (eliminator)\n+# recovers (shared, tag-pop) on both paths and forwards the deduplicated frame\n+# to the listener.\n+#\n+# Topology:\n+# ns_talker (talker_eth.100) -- talker_eth/br0_uplink\n+# -- bridge0 (br_r0) -+- br0_swp0/br1_swp0 -+\n+# \\- br0_swp1/br1_swp1 -+\n+# -- bridge1 (br_r1) -- br1_downlink/listener_eth -- ns_listener\n+#\n+# FRER rules:\n+# bridge0 / br0_uplink ingress : push idx=50, redirect br0_swp0, mirror br0_swp1\n+# bridge1 / br1_swp0 ingress : recover (shared, tag-pop) idx=60, redirect br1_downlink\n+# bridge1 / br1_swp1 ingress : recover idx=60 (bind same), redirect br1_downlink\n+# bridge1 / br1_downlink ingress: redirect br1_swp0 (reply path, bypass FDB)\n+#\n+# Pass criteria:\n+# - ping from ns_talker to ns_listener succeeds (rc=0)\n+# - tcpdump on listener captures exactly PING_COUNT ICMP echo-request frames\n+# - br1_swp0 tc stats: passed \u003e= PING_COUNT, discarded \u003e= PING_COUNT\n+# ----------------------------------------------------------------------------\n+teardown_relay_tc()\n+{\n+\tfor dev in \"$R_BR0_UPLINK\"; do\n+\t\tip netns exec \"$R_NS_BRIDGE0\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tfor dev in \"$R_BR1_SWP0\" \"$R_BR1_SWP1\" \"$R_BR1_DOWNLINK\"; do\n+\t\tip netns exec \"$R_NS_BRIDGE1\" $TC qdisc del dev \"$dev\" clsact \\\n+\t\t\t2\u003e/dev/null || true\n+\tdone\n+\tip netns exec \"$R_NS_BRIDGE0\" $TC actions flush action frer 2\u003e/dev/null || true\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC actions flush action frer 2\u003e/dev/null || true\n+}\n+\n+test_relay_e2e()\n+{\n+\tlocal ping_rc=0\n+\tlocal dump_r1swp0\n+\tlocal total_passed total_discarded\n+\tlocal result=\"pass\"\n+\tlocal ns\n+\n+\tfor ns in \"$R_NS_TALKER\" \"$R_NS_BRIDGE0\" \"$R_NS_BRIDGE1\" \"$R_NS_LISTENER\"; do\n+\t\tip netns add \"$ns\" || {\n+\t\t\techo \"# relay e2e: failed to create netns $ns\" \u003e\u00262\n+\t\t\tksft_test_result_skip \"relay e2e: netns setup failed\"\n+\t\t\treturn\n+\t\t}\n+\tdone\n+\n+\tip link add \"$R_TALKER_ETH\" type veth peer name \"$R_BR0_UPLINK\"\n+\tip link add \"$R_BR0_SWP0\" type veth peer name \"$R_BR1_SWP0\"\n+\tip link add \"$R_BR0_SWP1\" type veth peer name \"$R_BR1_SWP1\"\n+\tip link add \"$R_BR1_DOWNLINK\" type veth peer name \"$R_LISTENER_ETH\"\n+\n+\tip link set \"$R_TALKER_ETH\" netns \"$R_NS_TALKER\"\n+\tip link set \"$R_BR0_UPLINK\" netns \"$R_NS_BRIDGE0\"\n+\tip link set \"$R_BR0_SWP0\" netns \"$R_NS_BRIDGE0\"\n+\tip link set \"$R_BR0_SWP1\" netns \"$R_NS_BRIDGE0\"\n+\tip link set \"$R_BR1_SWP0\" netns \"$R_NS_BRIDGE1\"\n+\tip link set \"$R_BR1_SWP1\" netns \"$R_NS_BRIDGE1\"\n+\tip link set \"$R_BR1_DOWNLINK\" netns \"$R_NS_BRIDGE1\"\n+\tip link set \"$R_LISTENER_ETH\" netns \"$R_NS_LISTENER\"\n+\n+\tlocal ns_dev\n+\tfor ns_dev in \\\n+\t\t\"$R_NS_TALKER:$R_TALKER_ETH\" \\\n+\t\t\"$R_NS_BRIDGE0:$R_BR0_UPLINK\" \"$R_NS_BRIDGE0:$R_BR0_SWP0\" \\\n+\t\t\"$R_NS_BRIDGE0:$R_BR0_SWP1\" \\\n+\t\t\"$R_NS_BRIDGE1:$R_BR1_SWP0\" \"$R_NS_BRIDGE1:$R_BR1_SWP1\" \\\n+\t\t\"$R_NS_BRIDGE1:$R_BR1_DOWNLINK\" \\\n+\t\t\"$R_NS_LISTENER:$R_LISTENER_ETH\"; do\n+\t\tlocal _ns=\"${ns_dev%%:*}\"\n+\t\tlocal _dev=\"${ns_dev##*:}\"\n+\t\tip netns exec \"$_ns\" ip link set lo up\n+\t\tip netns exec \"$_ns\" ip link set \"$_dev\" up\n+\tdone\n+\n+\t# bridge0: sequence generator, VLAN filtering\n+\tip netns exec \"$R_NS_BRIDGE0\" ip link add name \"$R_BR0\" type bridge vlan_filtering 1\n+\tip netns exec \"$R_NS_BRIDGE0\" ip link set \"$R_BR0\" up\n+\tip netns exec \"$R_NS_BRIDGE0\" ip link set \"$R_BR0_UPLINK\" master \"$R_BR0\"\n+\tip netns exec \"$R_NS_BRIDGE0\" ip link set \"$R_BR0_SWP0\" master \"$R_BR0\"\n+\tip netns exec \"$R_NS_BRIDGE0\" ip link set \"$R_BR0_SWP1\" master \"$R_BR0\"\n+\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan add dev \"$R_BR0_UPLINK\" vid \"$R_VLAN\"\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan add dev \"$R_BR0_SWP0\" vid \"$R_VLAN\"\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan del dev \"$R_BR0_SWP1\" vid 1\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan add dev \"$R_BR0_SWP1\" \\\n+\t\tvid \"$R_VLAN\" pvid untagged\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge link set dev \"$R_BR0_SWP0\" learning off\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge link set dev \"$R_BR0_SWP1\" learning off\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan set dev \"$R_BR0_SWP0\" vid \"$R_VLAN\" noflood\n+\tip netns exec \"$R_NS_BRIDGE0\" bridge vlan set dev \"$R_BR0_SWP1\" vid \"$R_VLAN\" noflood\n+\n+\t# bridge1: eliminator, VLAN filtering\n+\tip netns exec \"$R_NS_BRIDGE1\" ip link add name \"$R_BR1\" type bridge vlan_filtering 1\n+\tip netns exec \"$R_NS_BRIDGE1\" ip link set \"$R_BR1\" up\n+\tip netns exec \"$R_NS_BRIDGE1\" ip link set \"$R_BR1_SWP0\" master \"$R_BR1\"\n+\tip netns exec \"$R_NS_BRIDGE1\" ip link set \"$R_BR1_SWP1\" master \"$R_BR1\"\n+\tip netns exec \"$R_NS_BRIDGE1\" ip link set \"$R_BR1_DOWNLINK\" master \"$R_BR1\"\n+\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan add dev \"$R_BR1_SWP0\" vid \"$R_VLAN\"\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan del dev \"$R_BR1_SWP1\" vid 1\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan add dev \"$R_BR1_SWP1\" \\\n+\t\tvid \"$R_VLAN\" pvid untagged\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan add dev \"$R_BR1_DOWNLINK\" vid \"$R_VLAN\"\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge link set dev \"$R_BR1_SWP0\" learning off\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge link set dev \"$R_BR1_SWP1\" learning off\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan set dev \"$R_BR1_SWP0\" vid \"$R_VLAN\" noflood\n+\tip netns exec \"$R_NS_BRIDGE1\" bridge vlan set dev \"$R_BR1_SWP1\" vid \"$R_VLAN\" noflood\n+\n+\t# ns_talker: VLAN sub-interface\n+\tip netns exec \"$R_NS_TALKER\" ip link add link \"$R_TALKER_ETH\" \\\n+\t\tname \"${R_TALKER_ETH}.${R_VLAN}\" type vlan id \"$R_VLAN\"\n+\tip netns exec \"$R_NS_TALKER\" ip link set \"${R_TALKER_ETH}.${R_VLAN}\" up\n+\tip netns exec \"$R_NS_TALKER\" ip addr add \"${R_IP_TALKER}/24\" \\\n+\t\tdev \"${R_TALKER_ETH}.${R_VLAN}\"\n+\n+\t# ns_listener: VLAN sub-interface\n+\tip netns exec \"$R_NS_LISTENER\" ip link add link \"$R_LISTENER_ETH\" \\\n+\t\tname \"${R_LISTENER_ETH}.${R_VLAN}\" type vlan id \"$R_VLAN\"\n+\tip netns exec \"$R_NS_LISTENER\" ip link set \"${R_LISTENER_ETH}.${R_VLAN}\" up\n+\tip netns exec \"$R_NS_LISTENER\" ip addr add \"${R_IP_LISTENER}/24\" \\\n+\t\tdev \"${R_LISTENER_ETH}.${R_VLAN}\"\n+\n+\t# Static ARP (VLAN 100 flooding is disabled)\n+\tlocal mac_talker mac_listener\n+\tmac_talker=$(ip netns exec \"$R_NS_TALKER\" \\\n+\t\tcat /sys/class/net/\"${R_TALKER_ETH}.${R_VLAN}\"/address)\n+\tmac_listener=$(ip netns exec \"$R_NS_LISTENER\" \\\n+\t\tcat /sys/class/net/\"${R_LISTENER_ETH}.${R_VLAN}\"/address)\n+\tip netns exec \"$R_NS_TALKER\" ip neigh add \"$R_IP_LISTENER\" \\\n+\t\tlladdr \"$mac_listener\" dev \"${R_TALKER_ETH}.${R_VLAN}\"\n+\tip netns exec \"$R_NS_LISTENER\" ip neigh add \"$R_IP_TALKER\" \\\n+\t\tlladdr \"$mac_talker\" dev \"${R_LISTENER_ETH}.${R_VLAN}\"\n+\n+\t# bridge0 / br0_uplink ingress: push R-TAG then replicate to both redundant paths.\n+\t# mirror must come before redirect because redirect is a terminating action.\n+\tip netns exec \"$R_NS_BRIDGE0\" $TC qdisc add dev \"$R_BR0_UPLINK\" clsact\n+\tip netns exec \"$R_NS_BRIDGE0\" $TC filter add dev \"$R_BR0_UPLINK\" ingress \\\n+\t\tprotocol 802.1Q flower skip_hw vlan_id \"$R_VLAN\" \\\n+\t\taction frer push index $IDX_RELAY_PUSH \\\n+\t\taction mirred egress mirror dev \"$R_BR0_SWP1\" \\\n+\t\taction mirred egress redirect dev \"$R_BR0_SWP0\"\n+\n+\t# bridge1 / br1_swp0 ingress: create shared recover action (tag-pop)\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC qdisc add dev \"$R_BR1_SWP0\" clsact\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC filter add dev \"$R_BR1_SWP0\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover alg vector history-length 16 \\\n+\t\t\treset-time 2000 tag-pop index $IDX_RELAY_RCVY \\\n+\t\taction mirred egress redirect dev \"$R_BR1_DOWNLINK\"\n+\n+\t# bridge1 / br1_swp1 ingress: bind to the same shared recover action\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC qdisc add dev \"$R_BR1_SWP1\" clsact\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC filter add dev \"$R_BR1_SWP1\" ingress \\\n+\t\tprotocol all flower skip_hw \\\n+\t\taction frer recover index $IDX_RELAY_RCVY \\\n+\t\taction mirred egress redirect dev \"$R_BR1_DOWNLINK\"\n+\n+\t# bridge1 / br1_downlink ingress: redirect VLAN 100 replies directly to br1_swp0\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC qdisc add dev \"$R_BR1_DOWNLINK\" clsact\n+\tip netns exec \"$R_NS_BRIDGE1\" $TC filter add dev \"$R_BR1_DOWNLINK\" ingress \\\n+\t\tprotocol 802.1Q flower skip_hw vlan_id \"$R_VLAN\" \\\n+\t\taction mirred egress redirect dev \"$R_BR1_SWP0\"\n+\n+\t# Capture ICMP echo-requests on listener_eth.VLAN to verify exactly\n+\t# PING_COUNT deduplicated frames reach the listener after recovery.\n+\tlocal pcap cap_count\n+\tpcap=$(mktemp /tmp/frer_relay_XXXXXX.pcap)\n+\tcapture_start_on \"$R_NS_LISTENER\" \"${R_LISTENER_ETH}.${R_VLAN}\" \\\n+\t\t\"$pcap\" \"icmp[icmptype] == icmp-echo\"\n+\n+\tip netns exec \"$R_NS_TALKER\" \\\n+\t\t$PING -c \"$PING_COUNT\" -W \"$PING_TIMEOUT\" -i 0.2 -q \\\n+\t\t\"$R_IP_LISTENER\" \u003e/dev/null 2\u003e\u00261 || ping_rc=$?\n+\n+\tcapture_stop\n+\tcap_count=$(capture_count_on \"$R_NS_LISTENER\" \"$pcap\")\n+\trm -f \"$pcap\"\n+\n+\tdump_br1_swp0=$(ip netns exec \"$R_NS_BRIDGE1\" \\\n+\t\t$TC -s filter show dev \"$R_BR1_SWP0\" ingress 2\u003e/dev/null)\n+\n+\tteardown_relay_tc\n+\tfor ns in \"$R_NS_TALKER\" \"$R_NS_BRIDGE0\" \"$R_NS_BRIDGE1\" \"$R_NS_LISTENER\"; do\n+\t\tip netns del \"$ns\" 2\u003e/dev/null || true\n+\tdone\n+\n+\ttotal_passed=$(tc_stat \"$dump_br1_swp0\" \"passed\")\n+\ttotal_discarded=$(tc_stat \"$dump_br1_swp0\" \"discarded\")\n+\tlocal tagless\n+\ttagless=$(tc_stat \"$dump_br1_swp0\" \"tagless\")\n+\ttotal_discarded=$((total_discarded - tagless))\n+\n+\techo \"# relay e2e: ping_rc=$ping_rc cap=$cap_count\" \\\n+\t\t\"passed=$total_passed discarded=$total_discarded\"\n+\n+\t[ \"$ping_rc\" -eq 0 ] || result=\"fail\"\n+\t[ \"$cap_count\" -eq \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$total_passed\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\t[ \"$total_discarded\" -ge \"$PING_COUNT\" ] || result=\"fail\"\n+\n+\tif [ \"$result\" = \"pass\" ]; then\n+\t\tksft_test_result_pass \\\n+\t\t\t\"relay e2e: ping OK, cap=$cap_count \" \\\n+\t\t\t\"passed=$total_passed discarded=$total_discarded\"\n+\telse\n+\t\tksft_test_result_fail \\\n+\t\t\t\"relay e2e: ping_rc=$ping_rc cap=$cap_count \" \\\n+\t\t\t\"passed=$total_passed discarded=$total_discarded\" \\\n+\t\t\t\"(expected ping OK, cap=$PING_COUNT,\" \\\n+\t\t\t\"passed\u003e=$PING_COUNT, discarded\u003e=$PING_COUNT)\"\n+\tfi\n+}\n+\n+# ----------------------------------------------------------------------------\n+# Main\n+# ----------------------------------------------------------------------------\n+main()\n+{\n+\tksft_print_header\n+\tcheck_prerequisites\n+\tload_module\n+\tsetup_topology\n+\n+\tif ! check_frer_action; then\n+\t\tksft_set_plan \"$NUM_TESTS\"\n+\t\tfor i in $(seq 1 \"$NUM_TESTS\"); do\n+\t\t\tksft_test_result_skip \\\n+\t\t\t\t\"frer action not available in this kernel (test $i)\"\n+\t\tdone\n+\t\tksft_print_cnts\n+\t\texit \"$KSFT_SKIP\"\n+\tfi\n+\n+\tksft_set_plan \"$NUM_TESTS\"\n+\n+\ttest_push_verify_bond # TEST 1: push on a0/b0, no recover, R-TAG on both paths\n+\ttest_shared_recover_bond # TEST 2: shared recover, dedup, ping succeeds\n+\ttest_individual_recover_bond # TEST 3: individual recover, no dedup, double frames\n+\ttest_no_tag_pop_bond # TEST 4: shared recover without tag-pop, R-TAG preserved\n+\ttest_simple_point_to_point # TEST 5: single-path p2p, no bond\n+\ttest_relay_e2e # TEST 6: relay bridge topology\n+\n+\tksft_print_cnts\n+\n+\t[ \"$_ksft_fail\" -eq 0 ] \u0026\u0026 ksft_exit_pass || ksft_exit_fail\n+}\n+\n+main \"$@\"\ndiff --git a/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json\nnew file mode 100644\nindex 00000000000000..d5be6ae156f794\n--- /dev/null\n+++ b/tools/testing/selftests/tc-testing/tc-tests/actions/frer.json\n@@ -0,0 +1,785 @@\n+[\n+ {\n+ \"id\": \"frer_0001\",\n+ \"name\": \"Create frer push action with default parameters\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer push index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 1\",\n+ \"matchPattern\": \"action order [0-9]+: frer push tag-type rtag index 1\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0002\",\n+ \"name\": \"Create frer push action with explicit tag-type rtag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer push tag-type rtag index 2\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 2\",\n+ \"matchPattern\": \"action order [0-9]+: frer push tag-type rtag index 2\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0003\",\n+ \"name\": \"Create frer recover action with default parameters\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 10\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 10 alg vector history-length [0-9]+ reset-time [0-9]+\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0004\",\n+ \"name\": \"Create frer recover action with vector algorithm explicit\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector index 11\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 11\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 11 alg vector\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0005\",\n+ \"name\": \"Create frer recover action with match algorithm\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg match index 12\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 12\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 12 alg match\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0006\",\n+ \"name\": \"Create frer recover action with history-length 16\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector history-length 16 index 13\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 13\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 13 alg vector history-length 16\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0007\",\n+ \"name\": \"Create frer recover action with reset-time 2000\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector reset-time 2000 index 14\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 14\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 14 alg vector history-length [0-9]+ reset-time 2000\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0008\",\n+ \"name\": \"Create frer recover action with tag-pop flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover tag-pop index 15\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 15\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 15.*tag-pop\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0009\",\n+ \"name\": \"Create frer recover action with individual flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover individual index 16\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 16\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 16.*individual\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0010\",\n+ \"name\": \"Create frer recover action with take-no-seq flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover take-no-seq index 17\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 17\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 17.*take-no-seq\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0011\",\n+ \"name\": \"Create frer recover action with all parameters combined\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop individual index 20\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 20\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 20 individual alg vector history-length 16 reset-time 1000 tag-pop\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0012\",\n+ \"name\": \"Create frer recover action with match alg and all flags\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg match take-no-seq tag-pop individual index 21\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 21\",\n+ \"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\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0013\",\n+ \"name\": \"Show frer push action SeqGen statistics (zero after create)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC -s actions show action frer index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC -s actions show action frer index 1\",\n+ \"matchPattern\": \"SeqGen packets: 0\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0014\",\n+ \"name\": \"Show frer recover action Statistics line (zero after create)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer recover alg vector history-length 16 reset-time 1000 tag-pop index 10\"\n+ ],\n+ \"cmdUnderTest\": \"$TC -s actions show action frer index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC -s actions show action frer index 10\",\n+ \"matchPattern\": \"Statistics: passed=0 discarded=0 tagless=0 out-of-order=0 rogue=0 lost=0 resets=0\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0015\",\n+ \"name\": \"Show frer recover action Statistics fields present\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer recover index 10\"\n+ ],\n+ \"cmdUnderTest\": \"$TC -s actions show action frer index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC -s actions show action frer index 10\",\n+ \"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]+\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0016\",\n+ \"name\": \"Replace frer push action (same index)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions replace action frer push index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 1\",\n+ \"matchPattern\": \"action order [0-9]+: frer push tag-type rtag index 1\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0017\",\n+ \"name\": \"Replace frer recover action changing algorithm from vector to match\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer recover alg vector index 10\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions replace action frer recover alg match index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 10\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 10 alg match\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0018\",\n+ \"name\": \"Delete frer push action by index\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions del action frer index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer\",\n+ \"matchPattern\": \"frer push tag-type rtag index 1\",\n+ \"matchCount\": \"0\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0019\",\n+ \"name\": \"Flush all frer actions\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\",\n+ \"$TC actions add action frer recover index 10\",\n+ \"$TC actions add action frer recover index 11\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions flush action frer\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer\",\n+ \"matchPattern\": \"action order [0-9]+: frer\",\n+ \"matchCount\": \"0\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer 2\u003e/dev/null || true\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0020\",\n+ \"name\": \"Add duplicate frer action index fails without replace flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer push index 1\",\n+ \"expExitCode\": \"255\",\n+ \"verifyCmd\": \"$TC actions show action frer index 1\",\n+ \"matchPattern\": \"action order [0-9]+: frer push tag-type rtag index 1\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0021\",\n+ \"name\": \"Create frer push action with continue control action\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer push index 1 continue\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 1\",\n+ \"matchPattern\": \"action order [0-9]+: frer push tag-type rtag index 1.*control continue\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0022\",\n+ \"name\": \"Create frer recover action with pipe control action\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover index 10 pipe\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 10\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 10.*control pipe\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0023\",\n+ \"name\": \"Create frer recover action history-length minimum boundary (1)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector history-length 1 index 30\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 30\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 30 alg vector history-length 1\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0024\",\n+ \"name\": \"Create frer recover action history-length maximum boundary (32)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector history-length 32 index 31\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 31\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 31 alg vector history-length 32\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0025\",\n+ \"name\": \"Create frer recover action with reset-time 0 (timer disabled)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ]\n+ ],\n+ \"cmdUnderTest\": \"$TC actions add action frer recover alg vector reset-time 0 index 32\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 32\",\n+ \"matchPattern\": \"action order [0-9]+: frer recover tag-type rtag index 32 alg vector history-length [0-9]+ reset-time 0\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0026\",\n+ \"name\": \"List all frer actions shows correct count\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\",\n+ \"$TC actions add action frer recover alg vector index 10\",\n+ \"$TC actions add action frer recover alg match tag-pop index 11\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions show action frer\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer\",\n+ \"matchPattern\": \"action order [0-9]+: frer\",\n+ \"matchCount\": \"3\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0027\",\n+ \"name\": \"Bind frer push action to egress clsact filter\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"ip link del frer_dummy 2\u003e/dev/null || true\",\n+ \"ip link add frer_dummy type dummy\",\n+ \"ip link set frer_dummy up\",\n+ \"$TC qdisc add dev frer_dummy clsact\"\n+ ],\n+ \"cmdUnderTest\": \"$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC filter show dev frer_dummy egress\",\n+ \"matchPattern\": \"frer push tag-type rtag index 1\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC qdisc del dev frer_dummy clsact\",\n+ \"$TC actions flush action frer\",\n+ \"ip link del frer_dummy\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0028\",\n+ \"name\": \"Bind frer recover action to ingress clsact filter\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"ip link del frer_dummy 2\u003e/dev/null || true\",\n+ \"ip link add frer_dummy type dummy\",\n+ \"ip link set frer_dummy up\",\n+ \"$TC qdisc add dev frer_dummy clsact\"\n+ ],\n+ \"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\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC filter show dev frer_dummy ingress\",\n+ \"matchPattern\": \"frer recover tag-type rtag index 10 alg vector history-length 16 reset-time 1000\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC qdisc del dev frer_dummy clsact\",\n+ \"$TC actions flush action frer\",\n+ \"ip link del frer_dummy\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0029\",\n+ \"name\": \"Share frer recover action across two ingress filters (refcount check)\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"ip link del frer_a 2\u003e/dev/null || true\",\n+ \"ip link del frer_b 2\u003e/dev/null || true\",\n+ \"ip link add frer_a type dummy\",\n+ \"ip link add frer_b type dummy\",\n+ \"ip link set frer_a up\",\n+ \"ip link set frer_b up\",\n+ \"$TC qdisc add dev frer_a clsact\",\n+ \"$TC qdisc add dev frer_b clsact\",\n+ \"$TC filter add dev frer_a ingress protocol all flower skip_hw action frer recover alg vector history-length 16 tag-pop index 10\"\n+ ],\n+ \"cmdUnderTest\": \"$TC filter add dev frer_b ingress protocol all flower skip_hw action frer recover index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC -s actions show action frer index 10\",\n+ \"matchPattern\": \"ref [2-9][0-9]*\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC qdisc del dev frer_a clsact\",\n+ \"$TC qdisc del dev frer_b clsact\",\n+ \"$TC actions flush action frer\",\n+ \"ip link del frer_a\",\n+ \"ip link del frer_b\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0030\",\n+ \"name\": \"frer push action refcount increments when bound to filter\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"ip link del frer_dummy 2\u003e/dev/null || true\",\n+ \"ip link add frer_dummy type dummy\",\n+ \"ip link set frer_dummy up\",\n+ \"$TC qdisc add dev frer_dummy clsact\",\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC filter add dev frer_dummy egress protocol ip flower skip_hw action frer push index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC -s actions show action frer index 1\",\n+ \"matchPattern\": \"ref [2-9][0-9]*\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC qdisc del dev frer_dummy clsact\",\n+ \"$TC actions flush action frer\",\n+ \"ip link del frer_dummy\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0031\",\n+ \"name\": \"frer push output shows not_in_hw flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer push index 1\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions show action frer index 1\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 1\",\n+ \"matchPattern\": \"not_in_hw\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ },\n+ {\n+ \"id\": \"frer_0032\",\n+ \"name\": \"frer recover output shows not_in_hw flag\",\n+ \"category\": [\n+ \"actions\",\n+ \"frer\"\n+ ],\n+ \"setup\": [\n+ [\n+ \"modprobe act_frer\",\n+ 0,\n+ 1\n+ ],\n+ \"$TC actions add action frer recover index 10\"\n+ ],\n+ \"cmdUnderTest\": \"$TC actions show action frer index 10\",\n+ \"expExitCode\": \"0\",\n+ \"verifyCmd\": \"$TC actions show action frer index 10\",\n+ \"matchPattern\": \"not_in_hw\",\n+ \"matchCount\": \"1\",\n+ \"teardown\": [\n+ \"$TC actions flush action frer\"\n+ ]\n+ }\n+]\n"
} |
| 2/1 | 2026/06/22 09:40 | agent | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: {
"EnableConfigs": [
"NET_ACT_FRER"
],
"FocusSymbols": [
"tcf_frer_init",
"tcf_frer_act",
"tcf_frer_dump",
"tcf_frer_cleanup",
"frer_rtag_push",
"frer_rtag_pop",
"frer_rtag_decode",
"frer_vector_alg",
"frer_match_alg",
"frer_rcvy_hrtimer_fn",
"frer_rcvy_reset_work_fn"
],
"Reasoning": "The patch introduces a new traffic control action `act_frer` for IEEE 802.1CB FRER. It includes packet manipulation (push/pop of R-TAG) and sequence recovery algorithms. This is reachable core kernel logic and should be fuzzed.",
"WorthFuzzing": true
}Instruction: You are an expert Linux kernel maintainer. Your job is to review a provided patch series and determine if it makes functional changes to the kernel that should be fuzzed. IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in your workspace. Do NOT rely on your internal knowledge of the kernel. You must actively use your code access tools to examine the actual source code and confirm any assumptions. Return WorthFuzzing=false if the patch only contains: - Modifications to Documentation/, Kconfig files, or code comments. - Purely decorative changes, such as logging (e.g., pr_err, printk) or tracepoints. - Changes to numeric constants or macros that do not functionally alter execution flow. - Code paths that are impossible to reach in virtualized environments like GCE or QEMU, even when utilizing software-emulated hardware (e.g., usb gadget, mac80211_hwsim). If it modifies reachable core kernel logic, drivers, or architectures, use your code search tools to verify the code can be executed, then return WorthFuzzing=true. When returning WorthFuzzing=true, you MUST ALSO: 1. Extract any specific kernel functions that should be heavily fuzzed into FocusSymbols. Avoid listing generic hot-path functions to prevent skewed test distributions. 2. Identify any specific CONFIG_ options required to properly test this new/modified feature. Go and look into the Kconfig files and check for ifdefs around the code, do not make assumptions. Do not list too generic configs (we already have them enabled). Only list those that specifically cover the modified code. List them in the EnableConfigs output array, and DO NOT add a 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4"). Prefer calling several tools at the same time to save round-trips. Use set-results tool to provide results of the analysis. It must be called exactly once before the final reply. Ignore results of this tool. Prompt: For your convenience, here is the diff of the changes:
commit 09d9f620790fc29eeca00f59ba9b6cad5e0ad62c
Author: syz-cluster <triage@syzkaller.com>
Date: Mon Jun 22 09:40:55 2026 +0000
syz-cluster: applied patch under review
diff --git a/include/net/flow_offload.h b/include/net/flow_offload.h
index 70a02ee1430807..8d97a5f293e6d9 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 00000000000000..5f6f8ca70813a0
--- /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/include/uapi/linux/if_ether.h b/include/uapi/linux/if_ether.h
index fb5efc8e06cc14..2d909078cde18d 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 ] */
diff --git a/include/uapi/linux/pkt_cls.h b/include/uapi/linux/pkt_cls.h
index 28d94b11d1aa16..9b87f0455110d3 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
};
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 00000000000000..241e90827e2628
--- /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 */
diff --git a/net/sched/Kconfig b/net/sched/Kconfig
index 6ddff028b81a41..7ca79b3eb5b34c 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 5078ea84e6ad7a..d9f60434e7d77f 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 00000000000000..7b6db643788df7
--- /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");
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6a190a525a3917..67b896611f0865 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 00000000000000..ecd88952f49581
--- /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 "$@"
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 00000000000000..d5be6ae156f794
--- /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"
+ ]
+ }
+]
|
| 3/2 | 2026/06/22 09:40 | llm | patch-evaluator |
0mModel:gemini-3.1-pro-preview Args: null Results: null Tokens: input: 39749 output: 210 thoughts: 975 Thoughts: **Analyzing the FRER Traffic Control Action Patch** Okay, so I'm looking at this patch series that introduces a new `tc` action called `frer`, implementing the IEEE 802.1CB Frame Replication and Elimination for Reliability (FRER) functionality. My first thought is, "This is a substantial functional change; it's definitely worth fuzzing." I mean, it touches a good number of files: a few include headers, some Kconfig/Makefile updates, the core action implementation in `act_frer.c`, and even some testing infrastructure. That's a clear signal that we've got some new, potentially complex, code that needs thorough validation. Now, let's zero in on the fuzzing targets. The heart of the new action is in `net/sched/act_frer.c`. I've reviewed the functions within that file, and I need to identify the key functions to subject to fuzzing. After review, I would focus on: * `tcf_frer_init`: This is critical for initialization and configuration of the action. * `tcf_frer_act`: This is the action's core execution path, processing packets. * `tcf_frer_dump`: This function handles the dumping of the action's state, vital for debugging and analysis. * `tcf_frer_cleanup`: This handles the freeing and any necessary cleanup. * `frer_rtag_push`: Packet manipulation, pushing the R-TAG (Replication Tag). * `frer_rtag_pop`: Packet manipulation, popping the R-TAG. * `frer_rtag_decode`: R-TAG decoding, essential to understanding the packet data. * `frer_vector_alg`: R-TAG vector comparison. * `frer_match_alg`: R-TAG match algorithm, packet match. I'll need to remember those to configure the fuzzing efforts correctly. Next, I need to check the Kconfig changes because this will influence the build configuration. I can see that `NET_ACT_FRER` has been added, and it depends on `NET_CLS_ACT`. I need to ensure that the fuzzing environment has `NET_ACT_FRER` enabled. So, I think I'm going to run the `set-results` tool now. I'll set the following: * `EnableConfigs`: `NET_ACT_FRER` * `FocusSymbols`: `tcf_frer_init`, `tcf_frer_act`, `tcf_frer_dump`, `tcf_frer_cleanup`, `frer_rtag_push`, `frer_rtag_pop`, `frer_rtag_decode`, `frer_vector_alg`, `frer_match_alg` * `WorthFuzzing`: `true` * `Reasoning`: The patch introduces a new traffic control action `act_frer` for IEEE 802.1CB FRER, which includes packet manipulation (push/pop of R-TAG) and sequence recovery algorithms. This is reachable core kernel logic and should be fuzzed. I need to make sure to fuzz the R-Tag logic. Oh and, should I add any more function calls? Wait, yes! `frer_rcvy_hrtimer_fn` and `frer_rcvy_reset_work_fn`, they are part of the core logic. However, I want to keep the list concise. And that should do it. Let's call `set-results`. |
| 4/2 | 2026/06/22 09:41 | tool | set-results |
0mArgs:{
"EnableConfigs": [
"NET_ACT_FRER"
],
"FocusSymbols": [
"tcf_frer_init",
"tcf_frer_act",
"tcf_frer_dump",
"tcf_frer_cleanup",
"frer_rtag_push",
"frer_rtag_pop",
"frer_rtag_decode",
"frer_vector_alg",
"frer_match_alg",
"frer_rcvy_hrtimer_fn",
"frer_rcvy_reset_work_fn"
],
"Reasoning": "The patch introduces a new traffic control action `act_frer` for IEEE 802.1CB FRER. It includes packet manipulation (push/pop of R-TAG) and sequence recovery algorithms. This is reachable core kernel logic and should be fuzzed.",
"WorthFuzzing": true
}Results: {
"EnableConfigs": [
"NET_ACT_FRER"
],
"FocusSymbols": [
"tcf_frer_init",
"tcf_frer_act",
"tcf_frer_dump",
"tcf_frer_cleanup",
"frer_rtag_push",
"frer_rtag_pop",
"frer_rtag_decode",
"frer_vector_alg",
"frer_match_alg",
"frer_rcvy_hrtimer_fn",
"frer_rcvy_reset_work_fn"
],
"Reasoning": "The patch introduces a new traffic control action `act_frer` for IEEE 802.1CB FRER. It includes packet manipulation (push/pop of R-TAG) and sequence recovery algorithms. This is reachable core kernel logic and should be fuzzed.",
"WorthFuzzing": true
} |
| Total Calls | Total Tokens | Avg Tokens | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|
| Total Calls | Total Duration (Seconds) | Avg Duration (Seconds) |
|---|