[RFC PATCH net-next 1/1] net: dsa: qca8k: fix host FDB handling
From: Brandon Mahdavi <hidden>
Date: 2026-07-25 03:22:20
Also in:
lkml
Subsystem:
networking drivers, networking [dsa], the rest · Maintainers:
Andrew Lunn, "David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Vladimir Oltean, Linus Torvalds
DSA installs host FDB entries on CPU and DSA ports for addresses that are
reachable through software interfaces. The QCA8K ATU has one entry per
{MAC, VID}, which makes these entries unsuitable for multi-conduit
configurations: successive adds overwrite the destination port, while
combining CPU ports in the destination mask duplicates host-bound traffic.
Host entries are not required on QCA8K. Unknown unicast flooding already
sends host-bound traffic toward the CPU ports, and port membership limits
delivery to the conduit associated with the ingress port. Skip host FDB
entries on CPU and DSA ports.
A skipped host add can coincide with a station roaming from a user port to
an interface behind the CPU. In that case, a dynamically learned ATU entry
can still point to the old user port. Since that entry prevents unknown
unicast flooding, host-bound traffic is discarded by same-port filtering.
Purge the stale dynamic entry when skipping the host add, but preserve
deliberately configured static entries.
Also skip the corresponding host delete, so deleting the software entry
does not remove a legitimately learned entry.
Tested on IPQ4019 with assisted learning enabled: stale entries were
removed when a wireless station associated and downstream traffic recovered
immediately.
Link: https://github.com/openwrt/openwrt/issues/21317
Link: https://github.com/openwrt/openwrt/pull/24162
Co-developed-by: Olaf Marzocchi <redacted>
Signed-off-by: Olaf Marzocchi <redacted>
Signed-off-by: Brandon Mahdavi <redacted>
---
drivers/net/dsa/qca/qca8k-common.c | 56 ++++++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/drivers/net/dsa/qca/qca8k-common.c b/drivers/net/dsa/qca/qca8k-common.c
index 13005f10edb7..a3b5a756dbc2 100644
--- a/drivers/net/dsa/qca/qca8k-common.c
+++ b/drivers/net/dsa/qca/qca8k-common.c@@ -322,6 +322,36 @@ static int qca8k_fdb_search_and_del(struct qca8k_priv *priv, u8 port_mask, return ret; } +static int qca8k_fdb_purge_dynamic(struct qca8k_priv *priv, const u8 *mac, + u16 vid) +{ + struct qca8k_fdb fdb = { 0 }; + int ret; + + mutex_lock(&priv->reg_mutex); + + qca8k_fdb_write(priv, vid, 0, mac, 0); + ret = qca8k_fdb_access(priv, QCA8K_FDB_SEARCH, -1); + if (ret < 0) + goto exit; + + ret = qca8k_fdb_read(priv, &fdb); + if (ret < 0) + goto exit; + + /* Nothing to purge, or a deliberately configured static entry */ + if (!fdb.aging || fdb.aging == QCA8K_ATU_STATUS_STATIC) { + ret = 0; + goto exit; + } + + ret = qca8k_fdb_access(priv, QCA8K_FDB_PURGE, -1); + +exit: + mutex_unlock(&priv->reg_mutex); + return ret; +} + static int qca8k_vlan_access(struct qca8k_priv *priv, enum qca8k_vlan_cmd cmd, u16 vid) {
@@ -829,6 +859,29 @@ int qca8k_port_fdb_add(struct dsa_switch *ds, int port, struct qca8k_priv *priv = ds->priv; u16 port_mask = BIT(port); + /* The ATU forwards a unicast hit to every port set in the + * destination mask, so a host entry spanning multiple CPU ports + * would deliver one copy of each host-bound frame per CPU port. + * Skip host entries entirely: unknown unicast is already forwarded + * to all CPU ports by GLOBAL_FW_CTRL1 and filtered by the ingress + * port LOOKUP_MEMBER, so host-bound frames reach exactly one CPU + * port, the conduit of the ingress port. + */ + if (dsa_is_cpu_port(ds, port) || dsa_is_dsa_port(ds, port)) { + /* The bridge now sees {addr, vid} on a software port behind + * the CPU (e.g. a wireless interface). A dynamically-learned + * ATU entry may still point at the user port the station + * roamed away from; a unicast hit on it forwards host-bound + * frames to that stale port, where the ingress port filter + * discards them, instead of flooding them to the CPU. Purge + * such an entry so the address is unknown unicast again. + */ + if (!vid) + vid = QCA8K_PORT_VID_DEF; + + return qca8k_fdb_purge_dynamic(priv, addr, vid); + } + return qca8k_port_fdb_insert(priv, addr, port_mask, vid); }
@@ -839,6 +892,9 @@ int qca8k_port_fdb_del(struct dsa_switch *ds, int port, struct qca8k_priv *priv = ds->priv; u16 port_mask = BIT(port); + if (dsa_is_cpu_port(ds, port) || dsa_is_dsa_port(ds, port)) + return 0; + if (!vid) vid = QCA8K_PORT_VID_DEF;
--
2.43.0