Subject: [PATCH net-next 9/9] net: dsa: netc: implement dynamic FDB entry aging

From: [email-masked]
Date: Wed, 27 May 2026 18:02:17 +0800

From: Wei Fang <[email-masked]>

The NETC switch does not age out dynamic FDB entries automatically.
Without software management, stale entries persist after topology
changes and cause incorrect forwarding.

Add a delayed work that periodically removes entries that have not been
refreshed within the specified cycles. The effective aging time is:

  aging_time = fdbt_acteu_interval * fdbt_ageing_act_cnt

Default values are 3s interval and 100 cycles (300s total), matching
the IEEE 802.1Q default aging time.  The work starts when the first
port joins a bridge (tracked via br_cnt) and is cancelled when the
last port leaves.  All FDB operations are serialized under fdbt_lock.

Dynamic entries for a departing port are also flushed immediately in
port_bridge_leave() and netc_mac_link_down(), without waiting for the
next aging cycle.

Implement set_ageing_time and port_fast_age DSA operations to allow
the bridge layer to reconfigure aging parameters and trigger per-port
flushes on demand.

Signed-off-by: Wei Fang <[email-masked]>
---
 drivers/net/dsa/netc/netc_main.c   | 80 ++++++++++++++++++++++++++++++
 drivers/net/dsa/netc/netc_switch.h |  8 +++
 2 files changed, 88 insertions(+)

diff --git a/drivers/net/dsa/netc/netc_main.c b/drivers/net/dsa/netc/netc_main.c
index 1fe3b43e0459..cffa479e19b3 100644
--- a/drivers/net/dsa/netc/netc_main.c
+++ b/drivers/net/dsa/netc/netc_main.c
@@ -447,6 +447,26 @@ static void netc_free_ntmp_user(struct netc_switch *priv)
 	netc_free_ntmp_bitmaps(priv);
 }
 
+static void netc_clean_fdbt_ageing_entries(struct work_struct *work)
+{
+	struct delayed_work *dwork = to_delayed_work(work);
+	struct netc_switch *priv;
+
+	priv = container_of(dwork, struct netc_switch, fdbt_clean);
+
+	/* Update the activity element in FDB table */
+	mutex_lock(&priv->fdbt_lock);
+	ntmp_fdbt_update_activity_element(&priv->ntmp);
+	/* Delete the aging entries after the activity element is updated */
+	ntmp_fdbt_delete_aging_entries(&priv->ntmp,
+				       READ_ONCE(priv->fdbt_ageing_act_cnt));
+	mutex_unlock(&priv->fdbt_lock);
+
+	if (atomic_read(&priv->br_cnt))
+		schedule_delayed_work(&priv->fdbt_clean,
+				      READ_ONCE(priv->fdbt_acteu_interval));
+}
+
 static void netc_switch_dos_default_config(struct netc_switch *priv)
 {
 	struct netc_switch_regs *regs = &priv->regs;
@@ -860,6 +880,11 @@ static int netc_setup(struct dsa_switch *ds)
 
 	INIT_HLIST_HEAD(&priv->fdb_list);
 	mutex_init(&priv->fdbt_lock);
+	priv->fdbt_acteu_interval = NETC_FDBT_CLEAN_INTERVAL;
+	priv->fdbt_ageing_act_cnt = NETC_FDBT_AGEING_ACT_CNT;
+	atomic_set(&priv->br_cnt, 0);
+	INIT_DELAYED_WORK(&priv->fdbt_clean,
+			  netc_clean_fdbt_ageing_entries);
 	INIT_HLIST_HEAD(&priv->vlan_list);
 	mutex_init(&priv->vft_lock);
 
@@ -924,6 +949,7 @@ static void netc_teardown(struct dsa_switch *ds)
 {
 	struct netc_switch *priv = ds->priv;
 
+	disable_delayed_work_sync(&priv->fdbt_clean);
 	netc_destroy_all_lists(priv);
 	netc_free_host_flood_rules(priv);
 	netc_free_ntmp_user(priv);
@@ -1918,6 +1944,7 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 				 struct netlink_ext_ack *extack)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
+	struct netc_switch *priv = ds->priv;
 	u16 vlan_unaware_pvid;
 	int err;
 
@@ -1944,20 +1971,42 @@ static int netc_port_bridge_join(struct dsa_switch *ds, int port,
 	np->host_flood = NULL;
 	netc_port_wr(np, NETC_PIPFCR, 0);
 
+	if (atomic_inc_return(&priv->br_cnt) == 1)
+		schedule_delayed_work(&priv->fdbt_clean,
+				      READ_ONCE(priv->fdbt_acteu_interval));
+
 	return 0;
 }
 
+static void netc_port_remove_dynamic_entries(struct netc_port *np)
+{
+	struct netc_switch *priv = np->switch_priv;
+
+	/* Return if the port is not available */
+	if (!np->dp)
+		return;
+
+	mutex_lock(&priv->fdbt_lock);
+	ntmp_fdbt_delete_port_dynamic_entries(&priv->ntmp, np->dp->index);
+	mutex_unlock(&priv->fdbt_lock);
+}
+
 static void netc_port_bridge_leave(struct dsa_switch *ds, int port,
 				   struct dsa_bridge bridge)
 {
 	struct netc_port *np = NETC_PORT(ds, port);
 	struct net_device *ndev = np->dp->user;
+	struct netc_switch *priv = ds->priv;
 	u16 vlan_unaware_pvid;
 	bool mc, uc;
 
 	netc_port_set_mlo(np, MLO_DISABLE);
 	netc_port_set_pvid(np, NETC_STANDALONE_PVID);
 
+	if (atomic_dec_and_test(&priv->br_cnt))
+		cancel_delayed_work_sync(&priv->fdbt_clean);
+
+	netc_port_remove_dynamic_entries(np);
 	uc = ndev->flags & IFF_PROMISC;
 	mc = ndev->flags & (IFF_PROMISC | IFF_ALLMULTI);
 
@@ -1977,6 +2026,34 @@ static void netc_port_bridge_leave(struct dsa_switch *ds, int port,
 	netc_port_del_vlan_entry(np, vlan_unaware_pvid);
 }
 
+static int netc_set_ageing_time(struct dsa_switch *ds, unsigned int msecs)
+{
+	struct netc_switch *priv = ds->priv;
+	u32 secs = msecs / 1000;
+	u32 act_cnt, interval;
+
+	if (!secs)
+		secs = 1;
+
+	for (interval = 1; interval <= secs; interval++) {
+		act_cnt = secs / interval;
+		if (act_cnt <= FDBT_ACT_CNT)
+			break;
+	}
+
+	WRITE_ONCE(priv->fdbt_acteu_interval, (unsigned long)interval * HZ);
+	WRITE_ONCE(priv->fdbt_ageing_act_cnt, act_cnt);
+
+	return 0;
+}
+
+static void netc_port_fast_age(struct dsa_switch *ds, int port)
+{
+	struct netc_port *np = NETC_PORT(ds, port);
+
+	netc_port_remove_dynamic_entries(np);
+}
+
 static void netc_phylink_get_caps(struct dsa_switch *ds, int port,
 				  struct phylink_config *config)
 {
@@ -2231,6 +2308,7 @@ static void netc_mac_link_down(struct phylink_config *config,
 	np = NETC_PORT(dp->ds, dp->index);
 	netc_port_mac_rx_graceful_stop(np);
 	netc_port_mac_tx_graceful_stop(np);
+	netc_port_remove_dynamic_entries(np);
 }
 
 static const struct phylink_mac_ops netc_phylink_mac_ops = {
@@ -2260,6 +2338,8 @@ static const struct dsa_switch_ops netc_switch_ops = {
 	.port_vlan_del			= netc_port_vlan_del,
 	.port_bridge_join		= netc_port_bridge_join,
 	.port_bridge_leave		= netc_port_bridge_leave,
+	.set_ageing_time		= netc_set_ageing_time,
+	.port_fast_age			= netc_port_fast_age,
 	.get_pause_stats		= netc_port_get_pause_stats,
 	.get_rmon_stats			= netc_port_get_rmon_stats,
 	.get_eth_ctrl_stats		= netc_port_get_eth_ctrl_stats,
diff --git a/drivers/net/dsa/netc/netc_switch.h b/drivers/net/dsa/netc/netc_switch.h
index 982c8d3a3fbf..8ad828f7d28c 100644
--- a/drivers/net/dsa/netc/netc_switch.h
+++ b/drivers/net/dsa/netc/netc_switch.h
@@ -50,6 +50,9 @@
 /* PAUSE refresh threshold: send refresh when timer reaches this value */
 #define NETC_PAUSE_THRESH		0x7FFF
 
+#define NETC_FDBT_CLEAN_INTERVAL	(3 * HZ)
+#define NETC_FDBT_AGEING_ACT_CNT	100
+
 struct netc_switch;
 
 struct netc_switch_info {
@@ -124,6 +127,11 @@ struct netc_switch {
 	struct ntmp_user ntmp;
 	struct hlist_head fdb_list;
 	struct mutex fdbt_lock; /* FDB table lock */
+	struct delayed_work fdbt_clean;
+	/* (interval * act_cnt) is ageing time */
+	unsigned long fdbt_acteu_interval;
+	u8 fdbt_ageing_act_cnt; /* maximum is 127 */
+	atomic_t br_cnt;
 	struct hlist_head vlan_list;
 	struct mutex vft_lock; /* VLAN filter table lock */
 
-- 
2.34.1