From: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
To: netdev@vger.kernel.org, linux-kernel@vger.kernel.org,
linux-kselftest@vger.kernel.org
Cc: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, jhs@mojatatu.com, jiri@resnulli.us,
horms@kernel.org, shuah@kernel.org, vladimir.oltean@nxp.com,
vinicius.gomes@intel.com, fejes@inf.elte.hu,
xiaoliang.yang_1@nxp.com
Subject: [PATCH net-next 4/6] net: sched: act_frer: add FRER tc action
Date: Mon, 22 Jun 2026 17:21:16 +0800 [thread overview]
Message-ID: <20260622092118.6846-5-xiaoliang.yang_1@nxp.com> (raw)
In-Reply-To: <20260622092118.6846-1-xiaoliang.yang_1@nxp.com>
Introduce the FRER tc action for IEEE 802.1CB. This patch adds the
module skeleton, the shared sequence-generator infrastructure, the
TCA_FRER_FUNC_PUSH data path, and the TCA_FRER_FUNC_RECOVER data path.
Sequence generation (IEEE 802.1CB Section 7.4.1):
Each push action embeds a struct frer_seqgen directly in tcf_frer,
protected by a per-action spinlock. The sequence counter wraps at
65536 (16-bit R-TAG field). When a Talker chains "action frer push"
with "action mirred egress mirror", both the primary and the mirrored
frame carry the same R-TAG because mirred copies the already-modified
skb. No changes to act_mirred are required (Split function,
Section 7.7).
Sequence Recovery vs. Individual Recovery (IEEE 802.1CB Section 7.5):
Sequence Recovery (cross-port deduplication):
Multiple ingress filters on different ports share one recover
action by referencing the same action index. They all operate on
the same struct frer_rcvy embedded in that tcf_frer instance and
protected by a spinlock. A frame arriving on any port is checked
against the shared sequence history; the first copy passes and all
later copies with the same sequence number are discarded.
Individual Recovery (per-port independent deduplication):
Each action uses its own frer_rcvy embedded directly in tcf_frer.
Selected when the user sets the "individual" flag.
Recovery algorithms:
Vector (7.4.3.4, default): 32-bit history bit-vector, handles
out-of-order delivery within the window.
Match (7.4.3.5): remembers only the last accepted sequence number.
Reset timer:
An hrtimer fires after frerSeqRcvyResetMSec ms of inactivity.
CLOCK_MONOTONIC is used throughout. The reset runs in a workqueue
to avoid holding the spinlock in the hrtimer callback.
R-TAG wire format (IEEE 802.1CB 7.8, EtherType 0xF1C1):
[Dst MAC 6B][Src MAC 6B][Optional 802.1Q tag 4B][0xF1C1 2B]
[Reserved 2B][Sequence Number 2B][Encapsulated EtherType 2B][Payload]
Signed-off-by: Xiaoliang Yang <xiaoliang.yang_1@nxp.com>
---
include/net/flow_offload.h | 11 +
include/net/tc_act/tc_frer.h | 71 +++
net/sched/Kconfig | 16 +
net/sched/Makefile | 1 +
net/sched/act_frer.c | 835 +++++++++++++++++++++++++++++++++++
5 files changed, 934 insertions(+)
create mode 100644 include/net/tc_act/tc_frer.h
create mode 100644 net/sched/act_frer.c
diff --git a/include/net/flow_offload.h b/include/net/flow_offload.h
index 70a02ee14308..8d97a5f293e6 100644
--- a/include/net/flow_offload.h
+++ b/include/net/flow_offload.h
@@ -184,6 +184,7 @@ enum flow_action_id {
FLOW_ACTION_VLAN_PUSH_ETH,
FLOW_ACTION_VLAN_POP_ETH,
FLOW_ACTION_CONTINUE,
+ FLOW_ACTION_FRER,
NUM_FLOW_ACTIONS,
};
@@ -329,6 +330,16 @@ struct flow_action_entry {
struct { /* FLOW_ACTION_PPPOE_PUSH */
u16 sid;
} pppoe;
+ struct { /* FLOW_ACTION_FRER */
+ u8 func;
+ u8 tag_type;
+ bool individual;
+ u8 rcvy_alg;
+ u8 rcvy_history_len;
+ u32 rcvy_reset_msec;
+ bool tag_pop;
+ bool take_no_seq;
+ } frer;
};
struct flow_action_cookie *user_cookie; /* user defined action cookie */
};
diff --git a/include/net/tc_act/tc_frer.h b/include/net/tc_act/tc_frer.h
new file mode 100644
index 000000000000..5f6f8ca70813
--- /dev/null
+++ b/include/net/tc_act/tc_frer.h
@@ -0,0 +1,71 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/* Copyright 2026 NXP */
+
+#ifndef __NET_TC_FRER_H
+#define __NET_TC_FRER_H
+
+#include <net/act_api.h>
+#include <linux/tc_act/tc_frer.h>
+
+/**
+ * struct frer_seqgen - sequence number generator state (embedded in tcf_frer)
+ */
+struct frer_seqgen {
+ u32 gen_seq_num;
+ u64 seq_space; /* 1 << 16 */
+ spinlock_t lock; /* protects frer_seqgen state */
+ u64 stats_pkts; /* frerCpsSeqGenPackets */
+};
+
+/**
+ * struct frer_rcvy - sequence recovery state (embedded in tcf_frer)
+ *
+ */
+struct frer_rcvy {
+ u8 alg;
+ u8 history_len; /* 1-32 */
+ u32 reset_msec;
+ u64 seq_space;
+ u32 rcvy_seq_num;
+ u32 seq_history;
+ bool take_any;
+ bool take_no_seq;
+ struct hrtimer hrtimer;
+ spinlock_t lock; /* protects frer_rcvy state */
+ /* statistics */
+ u64 stats_tagless_pkts;
+ u64 stats_out_of_order_pkts;
+ u64 stats_rogue_pkts;
+ u64 stats_lost_pkts;
+ u64 stats_resets;
+ u64 stats_passed_pkts;
+ u64 stats_discarded_pkts;
+};
+
+/**
+ * struct tcf_frer - per tc_action FRER private data
+ */
+struct tcf_frer {
+ struct tc_action common;
+ u8 func;
+ u8 tag_type;
+ bool tag_pop;
+ bool individual; /* Individual Recovery flag */
+ /* push path */
+ struct frer_seqgen seqgen;
+ /* recover path */
+ struct frer_rcvy rcvy;
+};
+
+#define to_frer(a) ((struct tcf_frer *)(a))
+
+static inline bool is_tcf_frer(const struct tc_action *a)
+{
+#ifdef CONFIG_NET_CLS_ACT
+ if (a->ops && a->ops->id == TCA_ID_FRER)
+ return true;
+#endif
+ return false;
+}
+
+#endif /* __NET_TC_FRER_H */
diff --git a/net/sched/Kconfig b/net/sched/Kconfig
index 6ddff028b81a..7ca79b3eb5b3 100644
--- a/net/sched/Kconfig
+++ b/net/sched/Kconfig
@@ -939,6 +939,22 @@ config NET_ACT_GATE
To compile this code as a module, choose M here: the
module will be called act_gate.
+config NET_ACT_FRER
+ tristate "IEEE 802.1CB FRER tc action"
+ depends on NET_CLS_ACT
+ help
+ Say Y here to enable the IEEE 802.1CB FRER tc action. The action
+ implements the Sequence Generation Function (egress R-TAG insertion
+ with shared per-stream sequence counter) and the Sequence Recovery
+ Function (ingress duplicate detection and elimination) described in
+ IEEE 802.1CB-2017.
+
+ Both Sequence Recovery (cross-port shared state via rcvy-id) and
+ Individual Recovery (per-port independent state) are supported.
+
+ To compile this code as a module, choose M here: the
+ module will be called act_frer.
+
config NET_IFE_SKBMARK
tristate "Support to encoding decoding skb mark on IFE action"
depends on NET_ACT_IFE
diff --git a/net/sched/Makefile b/net/sched/Makefile
index 5078ea84e6ad..d9f60434e7d7 100644
--- a/net/sched/Makefile
+++ b/net/sched/Makefile
@@ -31,6 +31,7 @@ obj-$(CONFIG_NET_IFE_SKBTCINDEX) += act_meta_skbtcindex.o
obj-$(CONFIG_NET_ACT_TUNNEL_KEY)+= act_tunnel_key.o
obj-$(CONFIG_NET_ACT_CT) += act_ct.o
obj-$(CONFIG_NET_ACT_GATE) += act_gate.o
+obj-$(CONFIG_NET_ACT_FRER) += act_frer.o
obj-$(CONFIG_NET_SCH_FIFO) += sch_fifo.o
obj-$(CONFIG_NET_SCH_HTB) += sch_htb.o
obj-$(CONFIG_NET_SCH_HFSC) += sch_hfsc.o
diff --git a/net/sched/act_frer.c b/net/sched/act_frer.c
new file mode 100644
index 000000000000..7b6db643788d
--- /dev/null
+++ b/net/sched/act_frer.c
@@ -0,0 +1,835 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/* Copyright 2026 NXP */
+
+#include <linux/module.h>
+#include <linux/types.h>
+#include <linux/kernel.h>
+#include <linux/skbuff.h>
+#include <linux/rtnetlink.h>
+#include <linux/slab.h>
+#include <linux/spinlock.h>
+#include <linux/if_vlan.h>
+#include <linux/hrtimer.h>
+#include <linux/workqueue.h>
+#include <net/act_api.h>
+#include <net/netlink.h>
+#include <net/pkt_cls.h>
+#include <net/tc_act/tc_frer.h>
+
+/* ------------------------------------------------------------------ */
+/* R-TAG wire structures (IEEE 802.1CB 7.8) */
+/* ------------------------------------------------------------------ */
+
+struct r_tag {
+ __be16 reserved;
+ __be16 sequence_nr;
+ __be16 encap_proto;
+} __packed;
+
+static struct tc_action_ops act_frer_ops;
+
+/* ------------------------------------------------------------------ */
+/* Recovery reset machinery */
+/* ------------------------------------------------------------------ */
+
+struct frer_rcvy_work {
+ struct work_struct work;
+ struct frer_rcvy *rcvy;
+};
+
+static void frer_rcvy_reset(struct frer_rcvy *rcvy)
+{
+ if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG) {
+ rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+ rcvy->seq_history = 0;
+ }
+ rcvy->take_any = true;
+ rcvy->stats_resets++;
+}
+
+static void frer_rcvy_reset_work_fn(struct work_struct *work)
+{
+ struct frer_rcvy_work *rw =
+ container_of(work, struct frer_rcvy_work, work);
+ struct frer_rcvy *rcvy = rw->rcvy;
+
+ spin_lock_bh(&rcvy->lock);
+ frer_rcvy_reset(rcvy);
+ spin_unlock_bh(&rcvy->lock);
+ kfree(rw);
+}
+
+static enum hrtimer_restart frer_rcvy_hrtimer_fn(struct hrtimer *timer)
+{
+ struct frer_rcvy *rcvy =
+ container_of(timer, struct frer_rcvy, hrtimer);
+ struct frer_rcvy_work *rw;
+
+ /* Allocate in GFP_ATOMIC context; if it fails the state is not
+ * reset this cycle - the next frame will attempt again.
+ */
+ rw = kmalloc_obj(*rw);
+ if (rw) {
+ INIT_WORK(&rw->work, frer_rcvy_reset_work_fn);
+ rw->rcvy = rcvy;
+ schedule_work(&rw->work);
+ }
+ return HRTIMER_NORESTART;
+}
+
+static void frer_rcvy_timer_restart(struct frer_rcvy *rcvy)
+{
+ if (rcvy->reset_msec)
+ hrtimer_start(&rcvy->hrtimer,
+ ms_to_ktime(rcvy->reset_msec),
+ HRTIMER_MODE_REL_SOFT);
+}
+
+static void frer_rcvy_init_state(struct frer_rcvy *rcvy, u8 alg,
+ u8 history_len, u32 reset_msec,
+ bool take_no_seq)
+{
+ rcvy->alg = alg;
+ rcvy->history_len = history_len;
+ rcvy->reset_msec = reset_msec;
+ rcvy->seq_space = 1 << 16;
+ rcvy->take_no_seq = take_no_seq;
+ rcvy->take_any = true;
+ rcvy->rcvy_seq_num = (u32)(rcvy->seq_space - 1);
+ rcvy->seq_history = 0;
+ spin_lock_init(&rcvy->lock);
+ hrtimer_setup(&rcvy->hrtimer, frer_rcvy_hrtimer_fn, CLOCK_MONOTONIC,
+ HRTIMER_MODE_REL_SOFT);
+}
+
+/* ------------------------------------------------------------------ */
+/* R-TAG helpers */
+/* ------------------------------------------------------------------ */
+
+static int frer_rtag_push(struct sk_buff *skb, u16 seq_num)
+{
+ unsigned char *new_mac_header;
+ unsigned int data_offset;
+ unsigned int head_len;
+ struct vlan_ethhdr *vh;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+ __be16 saved_proto;
+
+ if (!skb_mac_header_was_set(skb))
+ return -EINVAL;
+
+ data_offset = skb->data - skb_mac_header(skb);
+
+ if (skb_cow_head(skb, data_offset + sizeof(*rtag)))
+ return -ENOMEM;
+
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ if (!pskb_may_pull(skb, sizeof(*vh)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ if (!pskb_may_pull(skb, sizeof(*eh)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ saved_proto = *proto_ptr;
+ *proto_ptr = htons(ETH_P_RTAG);
+
+ skb_push(skb, sizeof(*rtag));
+ skb_reset_mac_header(skb);
+
+ new_mac_header = skb_mac_header(skb);
+ memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+ skb->protocol = htons(ETH_P_RTAG);
+ skb_set_network_header(skb, head_len);
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+
+ /* Write R-TAG after the Ethernet / VLAN header */
+ rtag = (struct r_tag *)(new_mac_header + head_len);
+ rtag->reserved = 0;
+ rtag->sequence_nr = htons(seq_num);
+ rtag->encap_proto = saved_proto;
+
+ return 0;
+}
+
+static void frer_rtag_pop(struct sk_buff *skb)
+{
+ unsigned char *new_mac_header;
+ unsigned int data_offset;
+ unsigned int head_len;
+ struct vlan_ethhdr *vh;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+
+ data_offset = skb->data - skb_mac_header(skb);
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ if (*proto_ptr != htons(ETH_P_RTAG))
+ return;
+
+ rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+ *proto_ptr = rtag->encap_proto;
+
+ skb->protocol = rtag->encap_proto;
+
+ skb_postpull_rcsum(skb, rtag, sizeof(struct r_tag));
+ skb_pull(skb, sizeof(*rtag));
+ skb_reset_mac_header(skb);
+
+ new_mac_header = skb_mac_header(skb);
+ memmove(new_mac_header, (unsigned char *)eh, head_len);
+
+ skb_set_network_header(skb, head_len);
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+}
+
+static int frer_rtag_decode(struct sk_buff *skb, int *seq)
+{
+ unsigned int data_offset;
+ struct vlan_ethhdr *vh;
+ unsigned int head_len;
+ struct ethhdr *eh;
+ struct r_tag *rtag;
+ __be16 *proto_ptr;
+
+ if (!skb_mac_header_was_set(skb))
+ return -EINVAL;
+
+ data_offset = skb->data - skb_mac_header(skb);
+
+ if (skb_cow_head(skb, data_offset))
+ return -ENOMEM;
+
+ if (data_offset > 0)
+ skb_push(skb, data_offset);
+
+ eh = eth_hdr(skb);
+ if (eth_type_vlan(eh->h_proto)) {
+ if (!pskb_may_pull(skb, sizeof(*vh) + sizeof(*rtag)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ vh = (struct vlan_ethhdr *)eh;
+ proto_ptr = &vh->h_vlan_encapsulated_proto;
+ head_len = sizeof(*vh);
+ } else {
+ if (!pskb_may_pull(skb, sizeof(*eh) + sizeof(*rtag)))
+ return -EINVAL;
+ eh = eth_hdr(skb);
+ proto_ptr = &eh->h_proto;
+ head_len = sizeof(*eh);
+ }
+
+ if (data_offset > 0)
+ skb_pull(skb, data_offset);
+
+ if (*proto_ptr != htons(ETH_P_RTAG)) {
+ *seq = -1;
+ return 0;
+ }
+
+ rtag = (struct r_tag *)((unsigned char *)eh + head_len);
+
+ *seq = (int)ntohs(rtag->sequence_nr);
+
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Recovery algorithms (called with rcvy->lock held) */
+/* ------------------------------------------------------------------ */
+
+/* Returns true = pass frame, false = discard frame.
+ * @individual: when true, restart the reset timer even on discarded frames
+ * (rogue/duplicate), as required for Individual Recovery (IEEE 802.1CB 7.5).
+ */
+static bool frer_vector_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+ int delta;
+ bool restart_timer = false;
+ bool pass;
+
+ if (seq < 0) {
+ /* No R-TAG present */
+ rcvy->stats_tagless_pkts++;
+ if (rcvy->take_no_seq) {
+ restart_timer = true;
+ pass = true;
+ } else {
+ pass = false;
+ }
+ goto out;
+ }
+
+ if (rcvy->take_any) {
+ /* First frame after reset: accept unconditionally */
+ rcvy->take_any = false;
+ rcvy->rcvy_seq_num = (u32)seq;
+ rcvy->seq_history = BIT(0);
+ restart_timer = true;
+ pass = true;
+ goto out;
+ }
+
+ delta = (seq - (int)rcvy->rcvy_seq_num) &
+ (int)(rcvy->seq_space - 1);
+ /* Map delta > seq_space/2 to negative (signed wrap) */
+ if ((u32)delta & (u32)(rcvy->seq_space / 2))
+ delta -= (int)rcvy->seq_space;
+
+ if (delta >= (int)rcvy->history_len ||
+ delta <= -(int)rcvy->history_len) {
+ /* Packet is out-of-range (rogue). */
+ rcvy->stats_rogue_pkts++;
+ if (individual)
+ restart_timer = true;
+ pass = false;
+ goto out;
+ }
+
+ if (delta <= 0) {
+ /* Packet is old: check whether already seen. */
+ if (rcvy->seq_history & BIT(-delta)) {
+ if (individual)
+ restart_timer = true;
+ /* Already received */
+ pass = false;
+ } else {
+ /* Out-of-order but not yet seen */
+ rcvy->seq_history |= BIT(-delta);
+ rcvy->stats_out_of_order_pkts++;
+ restart_timer = true;
+ pass = true;
+ }
+ goto out;
+ }
+
+ /* delta > 0: frame is newer than expected */
+ if (delta != 1)
+ rcvy->stats_out_of_order_pkts++;
+
+ /* Shift history forward, counting any gaps as lost */
+ while (--delta) {
+ if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+ rcvy->stats_lost_pkts++;
+ rcvy->seq_history <<= 1;
+ }
+ if (!(rcvy->seq_history & BIT(rcvy->history_len - 1)))
+ rcvy->stats_lost_pkts++;
+ rcvy->seq_history = (rcvy->seq_history << 1) | BIT(0);
+ rcvy->rcvy_seq_num = (u32)seq;
+ restart_timer = true;
+ pass = true;
+
+out:
+ if (restart_timer)
+ frer_rcvy_timer_restart(rcvy);
+ return pass;
+}
+
+static bool frer_match_alg(struct frer_rcvy *rcvy, int seq, bool individual)
+{
+ if (seq < 0) {
+ /* No R-TAG: Match alg cannot deduplicate, always pass. */
+ rcvy->stats_tagless_pkts++;
+ return true;
+ }
+
+ if (rcvy->take_any) {
+ rcvy->take_any = false;
+ rcvy->rcvy_seq_num = (u32)seq;
+ frer_rcvy_timer_restart(rcvy);
+ return true;
+ }
+
+ if ((u32)seq == rcvy->rcvy_seq_num) {
+ /* Duplicate */
+ if (individual)
+ frer_rcvy_timer_restart(rcvy);
+ return false;
+ }
+
+ /* New sequence number: accept and update */
+ if ((u32)seq != ((rcvy->rcvy_seq_num + 1) % rcvy->seq_space))
+ rcvy->stats_out_of_order_pkts++;
+ rcvy->rcvy_seq_num = (u32)seq;
+ frer_rcvy_timer_restart(rcvy);
+ return true;
+}
+
+/* ------------------------------------------------------------------ */
+/* Netlink policy */
+/* ------------------------------------------------------------------ */
+
+static const struct nla_policy frer_policy[TCA_FRER_MAX + 1] = {
+ [TCA_FRER_PARMS] = NLA_POLICY_EXACT_LEN(sizeof(struct tc_frer)),
+ [TCA_FRER_FUNC] = { .type = NLA_U8 },
+ [TCA_FRER_TAG_TYPE] = { .type = NLA_U8 },
+ [TCA_FRER_RCVY_INDIVIDUAL] = { .type = NLA_FLAG },
+ [TCA_FRER_RCVY_ALG] = { .type = NLA_U8 },
+ [TCA_FRER_RCVY_HISTORY_LEN] = NLA_POLICY_RANGE(NLA_U8, 1, 32),
+ [TCA_FRER_RCVY_RESET_MSEC] = { .type = NLA_U32 },
+ [TCA_FRER_RCVY_TAKE_NO_SEQ] = { .type = NLA_FLAG },
+ [TCA_FRER_RCVY_TAG_POP] = { .type = NLA_FLAG },
+};
+
+/* ------------------------------------------------------------------ */
+/* Action init */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_init(struct net *net, struct nlattr *nla,
+ struct nlattr *est, struct tc_action **a,
+ struct tcf_proto *tp, u32 flags,
+ struct netlink_ext_ack *extack)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+ bool bind = flags & TCA_ACT_FLAGS_BIND;
+ struct nlattr *tb[TCA_FRER_MAX + 1];
+ struct tcf_chain *goto_ch = NULL;
+ struct tcf_frer *f;
+ struct tc_frer *parm;
+ bool exists = false;
+ int ret = 0, err, index;
+ u8 func, tag_type;
+
+ if (!nla) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: attributes required");
+ return -EINVAL;
+ }
+
+ err = nla_parse_nested(tb, TCA_FRER_MAX, nla, frer_policy, extack);
+ if (err < 0)
+ return err;
+
+ if (!tb[TCA_FRER_PARMS]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_PARMS missing");
+ return -EINVAL;
+ }
+ if (!tb[TCA_FRER_FUNC]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_FUNC missing");
+ return -EINVAL;
+ }
+ if (!tb[TCA_FRER_TAG_TYPE]) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: TCA_FRER_TAG_TYPE missing");
+ return -EINVAL;
+ }
+
+ func = nla_get_u8(tb[TCA_FRER_FUNC]);
+ tag_type = nla_get_u8(tb[TCA_FRER_TAG_TYPE]);
+
+ if (func != TCA_FRER_FUNC_PUSH && func != TCA_FRER_FUNC_RECOVER) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: unknown func");
+ return -EINVAL;
+ }
+ if (tag_type != TCA_FRER_TAG_RTAG) {
+ NL_SET_ERR_MSG_MOD(extack, "frer: only rtag supported");
+ return -EOPNOTSUPP;
+ }
+
+ parm = nla_data(tb[TCA_FRER_PARMS]);
+ index = parm->index;
+
+ err = tcf_idr_check_alloc(tn, &index, a, bind);
+ if (err < 0)
+ return err;
+ exists = err;
+
+ if (exists && bind)
+ return ACT_P_BOUND;
+
+ if (!exists) {
+ ret = tcf_idr_create_from_flags(tn, index, est, a,
+ &act_frer_ops, bind, flags);
+ if (ret) {
+ tcf_idr_cleanup(tn, index);
+ return ret;
+ }
+ ret = ACT_P_CREATED;
+ } else if (!(flags & TCA_ACT_FLAGS_REPLACE)) {
+ tcf_idr_release(*a, bind);
+ return -EEXIST;
+ }
+
+ err = tcf_action_check_ctrlact(parm->action, tp, &goto_ch, extack);
+ if (err < 0)
+ goto release_idr;
+
+ f = to_frer(*a);
+
+ spin_lock_bh(&f->tcf_lock);
+ goto_ch = tcf_action_set_ctrlact(*a, parm->action, goto_ch);
+ f->func = func;
+ f->tag_type = tag_type;
+ f->tag_pop = !!tb[TCA_FRER_RCVY_TAG_POP];
+
+ if (func == TCA_FRER_FUNC_PUSH) {
+ if (ret == ACT_P_CREATED) {
+ spin_lock_init(&f->seqgen.lock);
+ f->seqgen.seq_space = 1 << 16;
+ }
+ /* gen_seq_num starts at 0 on creation; preserved on replace */
+ } else {
+ u8 alg = tb[TCA_FRER_RCVY_ALG] ?
+ nla_get_u8(tb[TCA_FRER_RCVY_ALG]) :
+ TCA_FRER_RCVY_VECTOR_ALG;
+ u8 history_len = tb[TCA_FRER_RCVY_HISTORY_LEN] ?
+ nla_get_u8(tb[TCA_FRER_RCVY_HISTORY_LEN]) : 32;
+ u32 reset_msec = tb[TCA_FRER_RCVY_RESET_MSEC] ?
+ nla_get_u32(tb[TCA_FRER_RCVY_RESET_MSEC]) : 0;
+ bool take_no_seq = !!tb[TCA_FRER_RCVY_TAKE_NO_SEQ];
+
+ if (alg != TCA_FRER_RCVY_VECTOR_ALG &&
+ alg != TCA_FRER_RCVY_MATCH_ALG) {
+ spin_unlock_bh(&f->tcf_lock);
+ NL_SET_ERR_MSG_MOD(extack, "frer: unknown recovery algorithm");
+ err = -EINVAL;
+ goto release_idr;
+ }
+
+ f->individual = !!tb[TCA_FRER_RCVY_INDIVIDUAL];
+
+ /* Cancel any running reset timer before re-initialising. */
+ if (ret != ACT_P_CREATED && f->rcvy.reset_msec) {
+ spin_unlock_bh(&f->tcf_lock);
+ hrtimer_cancel(&f->rcvy.hrtimer);
+ spin_lock_bh(&f->tcf_lock);
+ }
+
+ frer_rcvy_init_state(&f->rcvy, alg, history_len,
+ reset_msec, take_no_seq);
+ }
+
+ spin_unlock_bh(&f->tcf_lock);
+
+ if (goto_ch)
+ tcf_chain_put_by_act(goto_ch);
+
+ return ret;
+
+release_idr:
+ tcf_idr_release(*a, bind);
+ return err;
+}
+
+/* ------------------------------------------------------------------ */
+/* Data path */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_act(struct sk_buff *skb, const struct tc_action *a,
+ struct tcf_result *res)
+{
+ struct tcf_frer *f = to_frer(a);
+ int retval;
+
+ tcf_lastuse_update(&f->tcf_tm);
+ tcf_action_update_bstats(&f->common, skb);
+ retval = READ_ONCE(f->tcf_action);
+
+ if (f->func == TCA_FRER_FUNC_PUSH) {
+ struct frer_seqgen *sg = &f->seqgen;
+ u16 seq;
+
+ spin_lock(&sg->lock);
+ seq = (u16)sg->gen_seq_num;
+ if (++sg->gen_seq_num >= sg->seq_space)
+ sg->gen_seq_num = 0;
+ sg->stats_pkts++;
+ spin_unlock(&sg->lock);
+
+ if (frer_rtag_push(skb, seq) < 0) {
+ tcf_action_inc_drop_qstats(&f->common);
+ return TC_ACT_SHOT;
+ }
+ } else {
+ struct frer_rcvy *rcvy = &f->rcvy;
+ bool pass;
+ int seq;
+
+ if (frer_rtag_decode(skb, &seq) < 0) {
+ tcf_action_inc_drop_qstats(&f->common);
+ return TC_ACT_SHOT;
+ }
+
+ spin_lock(&rcvy->lock);
+ if (rcvy->alg == TCA_FRER_RCVY_VECTOR_ALG)
+ pass = frer_vector_alg(rcvy, seq, f->individual);
+ else
+ pass = frer_match_alg(rcvy, seq, f->individual);
+
+ if (pass) {
+ rcvy->stats_passed_pkts++;
+ spin_unlock(&rcvy->lock);
+ if (f->tag_pop)
+ frer_rtag_pop(skb);
+ return retval;
+ }
+
+ rcvy->stats_discarded_pkts++;
+ spin_unlock(&rcvy->lock);
+ return TC_ACT_SHOT;
+ }
+
+ return retval;
+}
+
+/* ------------------------------------------------------------------ */
+/* Dump */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_dump(struct sk_buff *skb, struct tc_action *a,
+ int bind, int ref)
+{
+ unsigned char *b = skb_tail_pointer(skb);
+ struct tcf_frer *f = to_frer(a);
+ struct tc_frer opt = {
+ .index = f->tcf_index,
+ .refcnt = refcount_read(&f->tcf_refcnt) - ref,
+ .bindcnt = atomic_read(&f->tcf_bindcnt) - bind,
+ };
+ struct tcf_t t;
+
+ spin_lock_bh(&f->tcf_lock);
+ opt.action = f->tcf_action;
+
+ if (nla_put(skb, TCA_FRER_PARMS, sizeof(opt), &opt))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_FUNC, f->func))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_TAG_TYPE, f->tag_type))
+ goto nla_put_failure;
+ if (f->tag_pop && nla_put_flag(skb, TCA_FRER_RCVY_TAG_POP))
+ goto nla_put_failure;
+
+ if (f->func == TCA_FRER_FUNC_PUSH) {
+ spin_lock(&f->seqgen.lock);
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_SEQGEN_PKTS,
+ f->seqgen.stats_pkts, TCA_FRER_PAD)) {
+ spin_unlock(&f->seqgen.lock);
+ goto nla_put_failure;
+ }
+ spin_unlock(&f->seqgen.lock);
+ } else {
+ u64 tagless, ooo, rogue, lost, resets, passed, discarded;
+ struct frer_rcvy *rcvy = &f->rcvy;
+
+ spin_lock(&rcvy->lock);
+ tagless = rcvy->stats_tagless_pkts;
+ ooo = rcvy->stats_out_of_order_pkts;
+ rogue = rcvy->stats_rogue_pkts;
+ lost = rcvy->stats_lost_pkts;
+ resets = rcvy->stats_resets;
+ passed = rcvy->stats_passed_pkts;
+ discarded = rcvy->stats_discarded_pkts;
+ spin_unlock(&rcvy->lock);
+
+ if (f->individual && nla_put_flag(skb, TCA_FRER_RCVY_INDIVIDUAL))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_RCVY_ALG, rcvy->alg))
+ goto nla_put_failure;
+ if (nla_put_u8(skb, TCA_FRER_RCVY_HISTORY_LEN, rcvy->history_len))
+ goto nla_put_failure;
+ if (nla_put_u32(skb, TCA_FRER_RCVY_RESET_MSEC, rcvy->reset_msec))
+ goto nla_put_failure;
+ if (rcvy->take_no_seq && nla_put_flag(skb, TCA_FRER_RCVY_TAKE_NO_SEQ))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_TAGLESS_PKTS,
+ tagless, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_OUT_OF_ORDER_PKTS,
+ ooo, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_ROGUE_PKTS,
+ rogue, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_LOST_PKTS,
+ lost, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_RESETS,
+ resets, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_PASSED_PKTS,
+ passed, TCA_FRER_PAD))
+ goto nla_put_failure;
+ if (nla_put_u64_64bit(skb, TCA_FRER_STATS_DISCARDED_PKTS,
+ discarded, TCA_FRER_PAD))
+ goto nla_put_failure;
+ }
+
+ tcf_tm_dump(&t, &f->tcf_tm);
+ if (nla_put_64bit(skb, TCA_FRER_TM, sizeof(t), &t, TCA_FRER_PAD))
+ goto nla_put_failure;
+
+ spin_unlock_bh(&f->tcf_lock);
+ return skb->len;
+
+nla_put_failure:
+ spin_unlock_bh(&f->tcf_lock);
+ nlmsg_trim(skb, b);
+ return -1;
+}
+
+/* ------------------------------------------------------------------ */
+/* Cleanup */
+/* ------------------------------------------------------------------ */
+
+static void tcf_frer_cleanup(struct tc_action *a)
+{
+ struct tcf_frer *f = to_frer(a);
+
+ if (f->func == TCA_FRER_FUNC_RECOVER)
+ hrtimer_cancel(&f->rcvy.hrtimer);
+}
+
+/* ------------------------------------------------------------------ */
+/* Walker / search / stats / fill-size / offload */
+/* ------------------------------------------------------------------ */
+
+static int tcf_frer_walker(struct net *net, struct sk_buff *skb,
+ struct netlink_callback *cb, int type,
+ const struct tc_action_ops *ops,
+ struct netlink_ext_ack *extack)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+ return tcf_generic_walker(tn, skb, cb, type, ops, extack);
+}
+
+static void tcf_frer_stats_update(struct tc_action *a, u64 bytes, u64 packets,
+ u64 drops, u64 lastuse, bool hw)
+{
+ struct tcf_frer *f = to_frer(a);
+ struct tcf_t *tm = &f->tcf_tm;
+
+ tcf_action_update_stats(a, bytes, packets, drops, hw);
+ tm->lastuse = max_t(u64, tm->lastuse, lastuse);
+}
+
+static size_t tcf_frer_get_fill_size(const struct tc_action *act)
+{
+ return nla_total_size(sizeof(struct tc_frer)) /* TCA_FRER_PARMS */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_FUNC */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_TAG_TYPE */
+ + nla_total_size(0) /* TCA_FRER_RCVY_TAG_POP (flag) */
+ + nla_total_size(0) /* TCA_FRER_RCVY_INDIVIDUAL (flag) */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_ALG */
+ + nla_total_size(sizeof(u8)) /* TCA_FRER_RCVY_HISTORY_LEN */
+ + nla_total_size(sizeof(u32)) /* TCA_FRER_RCVY_RESET_MSEC */
+ + nla_total_size(0) /* TCA_FRER_RCVY_TAKE_NO_SEQ (flag) */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_TAGLESS_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_OUT_OF_ORDER_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_ROGUE_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_LOST_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_RESETS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_PASSED_PKTS */
+ + nla_total_size_64bit(sizeof(u64)) /* TCA_FRER_STATS_DISCARDED_PKTS */
+ + nla_total_size_64bit(sizeof(struct tcf_t)); /* TCA_FRER_TM */
+}
+
+static int tcf_frer_offload_act_setup(struct tc_action *act, void *entry_data,
+ u32 *index_inc, bool bind,
+ struct netlink_ext_ack *extack)
+{
+ if (bind) {
+ struct flow_action_entry *entry = entry_data;
+ struct tcf_frer *f = to_frer(act);
+
+ entry->id = FLOW_ACTION_FRER;
+ entry->frer.func = f->func;
+ entry->frer.tag_type = f->tag_type;
+ entry->frer.tag_pop = f->tag_pop;
+
+ if (f->func != TCA_FRER_FUNC_PUSH) {
+ entry->frer.individual = f->individual;
+ entry->frer.rcvy_alg = f->rcvy.alg;
+ entry->frer.rcvy_history_len = f->rcvy.history_len;
+ entry->frer.rcvy_reset_msec = f->rcvy.reset_msec;
+ entry->frer.take_no_seq = f->rcvy.take_no_seq;
+ }
+ *index_inc = 1;
+ } else {
+ struct flow_offload_action *fl_action = entry_data;
+
+ fl_action->id = FLOW_ACTION_FRER;
+ }
+ return 0;
+}
+
+/* ------------------------------------------------------------------ */
+/* Module glue */
+/* ------------------------------------------------------------------ */
+
+static struct tc_action_ops act_frer_ops = {
+ .kind = "frer",
+ .id = TCA_ID_FRER,
+ .owner = THIS_MODULE,
+ .act = tcf_frer_act,
+ .init = tcf_frer_init,
+ .cleanup = tcf_frer_cleanup,
+ .dump = tcf_frer_dump,
+ .walk = tcf_frer_walker,
+ .stats_update = tcf_frer_stats_update,
+ .get_fill_size = tcf_frer_get_fill_size,
+ .offload_act_setup = tcf_frer_offload_act_setup,
+ .size = sizeof(struct tcf_frer),
+};
+
+static __net_init int frer_init_net(struct net *net)
+{
+ struct tc_action_net *tn = net_generic(net, act_frer_ops.net_id);
+
+ return tc_action_net_init(net, tn, &act_frer_ops);
+}
+
+static void __net_exit frer_exit_net(struct list_head *net_list)
+{
+ tc_action_net_exit(net_list, act_frer_ops.net_id);
+}
+
+static struct pernet_operations frer_net_ops = {
+ .init = frer_init_net,
+ .exit_batch = frer_exit_net,
+ .id = &act_frer_ops.net_id,
+ .size = sizeof(struct tc_action_net),
+};
+
+static int __init frer_init_module(void)
+{
+ return tcf_register_action(&act_frer_ops, &frer_net_ops);
+}
+
+static void __exit frer_cleanup_module(void)
+{
+ tcf_unregister_action(&act_frer_ops, &frer_net_ops);
+}
+
+module_init(frer_init_module);
+module_exit(frer_cleanup_module);
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("IEEE 802.1CB FRER tc action");
--
2.17.1
next prev parent reply other threads:[~2026-06-22 9:20 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-22 9:21 [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Xiaoliang Yang
2026-06-22 9:21 ` [PATCH net-next 1/6] uapi: if_ether: add ETH_P_RTAG for IEEE 802.1CB R-TAG Xiaoliang Yang
2026-06-22 9:21 ` [PATCH net-next 2/6] uapi: pkt_cls: add TCA_ID_FRER action identifier Xiaoliang Yang
2026-06-22 9:21 ` [PATCH net-next 3/6] uapi: tc_act: add tc_frer UAPI header Xiaoliang Yang
2026-06-22 9:21 ` Xiaoliang Yang [this message]
2026-06-22 9:21 ` [PATCH net-next 5/6] selftest: add tc-testing JSON test cases for act_frer Xiaoliang Yang
2026-06-22 9:21 ` [PATCH net-next 6/6] selftests: net: add kselftest for IEEE 802.1CB FRER tc action Xiaoliang Yang
2026-06-22 15:59 ` [PATCH net-next 0/6] tc: introduce FRER action (IEEE 802.1CB) Jakub Kicinski
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260622092118.6846-5-xiaoliang.yang_1@nxp.com \
--to=xiaoliang.yang_1@nxp.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=fejes@inf.elte.hu \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shuah@kernel.org \
--cc=vinicius.gomes@intel.com \
--cc=vladimir.oltean@nxp.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox