--- /dev/null
+From 90dc8fd36078a536671adae884d0b929cce6480a Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:30 +0200
+Subject: [PATCH] net: bridge: notify switchdev of disappearance of old FDB
+ entry upon migration
+
+Currently the bridge emits atomic switchdev notifications for
+dynamically learnt FDB entries. Monitoring these notifications works
+wonders for switchdev drivers that want to keep their hardware FDB in
+sync with the bridge's FDB.
+
+For example station A wants to talk to station B in the diagram below,
+and we are concerned with the behavior of the bridge on the DUT device:
+
+ DUT
+ +-------------------------------------+
+ | br0 |
+ | +------+ +------+ +------+ +------+ |
+ | | | | | | | | | |
+ | | swp0 | | swp1 | | swp2 | | eth0 | |
+ +-------------------------------------+
+ | | |
+ Station A | |
+ | |
+ +--+------+--+ +--+------+--+
+ | | | | | | | |
+ | | swp0 | | | | swp0 | |
+ Another | +------+ | | +------+ | Another
+ switch | br0 | | br0 | switch
+ | +------+ | | +------+ |
+ | | | | | | | |
+ | | swp1 | | | | swp1 | |
+ +--+------+--+ +--+------+--+
+ |
+ Station B
+
+Interfaces swp0, swp1, swp2 are handled by a switchdev driver that has
+the following property: frames injected from its control interface bypass
+the internal address analyzer logic, and therefore, this hardware does
+not learn from the source address of packets transmitted by the network
+stack through it. So, since bridging between eth0 (where Station B is
+attached) and swp0 (where Station A is attached) is done in software,
+the switchdev hardware will never learn the source address of Station B.
+So the traffic towards that destination will be treated as unknown, i.e.
+flooded.
+
+This is where the bridge notifications come in handy. When br0 on the
+DUT sees frames with Station B's MAC address on eth0, the switchdev
+driver gets these notifications and can install a rule to send frames
+towards Station B's address that are incoming from swp0, swp1, swp2,
+only towards the control interface. This is all switchdev driver private
+business, which the notification makes possible.
+
+All is fine until someone unplugs Station B's cable and moves it to the
+other switch:
+
+ DUT
+ +-------------------------------------+
+ | br0 |
+ | +------+ +------+ +------+ +------+ |
+ | | | | | | | | | |
+ | | swp0 | | swp1 | | swp2 | | eth0 | |
+ +-------------------------------------+
+ | | |
+ Station A | |
+ | |
+ +--+------+--+ +--+------+--+
+ | | | | | | | |
+ | | swp0 | | | | swp0 | |
+ Another | +------+ | | +------+ | Another
+ switch | br0 | | br0 | switch
+ | +------+ | | +------+ |
+ | | | | | | | |
+ | | swp1 | | | | swp1 | |
+ +--+------+--+ +--+------+--+
+ |
+ Station B
+
+Luckily for the use cases we care about, Station B is noisy enough that
+the DUT hears it (on swp1 this time). swp1 receives the frames and
+delivers them to the bridge, who enters the unlikely path in br_fdb_update
+of updating an existing entry. It moves the entry in the software bridge
+to swp1 and emits an addition notification towards that.
+
+As far as the switchdev driver is concerned, all that it needs to ensure
+is that traffic between Station A and Station B is not forever broken.
+If it does nothing, then the stale rule to send frames for Station B
+towards the control interface remains in place. But Station B is no
+longer reachable via the control interface, but via a port that can
+offload the bridge port learning attribute. It's just that the port is
+prevented from learning this address, since the rule overrides FDB
+updates. So the rule needs to go. The question is via what mechanism.
+
+It sure would be possible for this switchdev driver to keep track of all
+addresses which are sent to the control interface, and then also listen
+for bridge notifier events on its own ports, searching for the ones that
+have a MAC address which was previously sent to the control interface.
+But this is cumbersome and inefficient. Instead, with one small change,
+the bridge could notify of the address deletion from the old port, in a
+symmetrical manner with how it did for the insertion. Then the switchdev
+driver would not be required to monitor learn/forget events for its own
+ports. It could just delete the rule towards the control interface upon
+bridge entry migration. This would make hardware address learning be
+possible again. Then it would take a few more packets until the hardware
+and software FDB would be in sync again.
+
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Acked-by: Nikolay Aleksandrov <nikolay@nvidia.com>
+Reviewed-by: Ido Schimmel <idosch@nvidia.com>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ net/bridge/br_fdb.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/net/bridge/br_fdb.c
++++ b/net/bridge/br_fdb.c
+@@ -581,6 +581,7 @@ void br_fdb_update(struct net_bridge *br
+
+ /* fastpath: update of existing entry */
+ if (unlikely(source != fdb->dst && !fdb->is_sticky)) {
++ br_switchdev_fdb_notify(fdb, RTM_DELNEIGH);
+ fdb->dst = source;
+ fdb_modified = true;
+ /* Take over HW learned entry */
--- /dev/null
+From 2fd186501b1cff155cc4a755c210793cfc0dffb5 Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:31 +0200
+Subject: [PATCH] net: dsa: be louder when a non-legacy FDB operation fails
+
+The dev_close() call was added in commit c9eb3e0f8701 ("net: dsa: Add
+support for learning FDB through notification") "to indicate inconsistent
+situation" when we could not delete an FDB entry from the port.
+
+bridge fdb del d8:58:d7:00:ca:6d dev swp0 self master
+
+It is a bit drastic and at the same time not helpful if the above fails
+to only print with netdev_dbg log level, but on the other hand to bring
+the interface down.
+
+So increase the verbosity of the error message, and drop dev_close().
+
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ net/dsa/slave.c | 10 +++++++---
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1590,7 +1590,9 @@ static void dsa_slave_switchdev_event_wo
+
+ err = dsa_port_fdb_add(dp, fdb_info->addr, fdb_info->vid);
+ if (err) {
+- netdev_dbg(dev, "fdb add failed err=%d\n", err);
++ netdev_err(dev,
++ "failed to add %pM vid %d to fdb: %d\n",
++ fdb_info->addr, fdb_info->vid, err);
+ break;
+ }
+ fdb_info->offloaded = true;
+@@ -1605,9 +1607,11 @@ static void dsa_slave_switchdev_event_wo
+
+ err = dsa_port_fdb_del(dp, fdb_info->addr, fdb_info->vid);
+ if (err) {
+- netdev_dbg(dev, "fdb del failed err=%d\n", err);
+- dev_close(dev);
++ netdev_err(dev,
++ "failed to delete %pM vid %d from fdb: %d\n",
++ fdb_info->addr, fdb_info->vid, err);
+ }
++
+ break;
+ }
+ rtnl_unlock();
--- /dev/null
+From c4bb76a9a0ef87c4cc1f636defed5f12deb9f5a7 Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:32 +0200
+Subject: [PATCH] net: dsa: don't use switchdev_notifier_fdb_info in
+ dsa_switchdev_event_work
+
+Currently DSA doesn't add FDB entries on the CPU port, because it only
+does so through switchdev, which is associated with a net_device, and
+there are none of those for the CPU port.
+
+But actually FDB addresses on the CPU port have some use cases of their
+own, if the switchdev operations are initiated from within the DSA
+layer. There is just one problem with the existing code: it passes a
+structure in dsa_switchdev_event_work which was retrieved directly from
+switchdev, so it contains a net_device. We need to generalize the
+contents to something that covers the CPU port as well: the "ds, port"
+tuple is fine for that.
+
+Note that the new procedure for notifying the successful FDB offload is
+inspired from the rocker model.
+
+Also, nothing was being done if added_by_user was false. Let's check for
+that a lot earlier, and don't actually bother to schedule the worker
+for nothing.
+
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ net/dsa/dsa_priv.h | 12 +++++
+ net/dsa/slave.c | 106 ++++++++++++++++++++++-----------------------
+ 2 files changed, 65 insertions(+), 53 deletions(-)
+
+--- a/net/dsa/dsa_priv.h
++++ b/net/dsa/dsa_priv.h
+@@ -62,6 +62,18 @@ struct dsa_notifier_vlan_info {
+ int port;
+ };
+
++struct dsa_switchdev_event_work {
++ struct dsa_switch *ds;
++ int port;
++ struct work_struct work;
++ unsigned long event;
++ /* Specific for SWITCHDEV_FDB_ADD_TO_DEVICE and
++ * SWITCHDEV_FDB_DEL_TO_DEVICE
++ */
++ unsigned char addr[ETH_ALEN];
++ u16 vid;
++};
++
+ struct dsa_slave_priv {
+ /* Copy of CPU port xmit for faster access in slave transmit hot path */
+ struct sk_buff * (*xmit)(struct sk_buff *skb,
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1565,76 +1565,66 @@ static int dsa_slave_netdevice_event(str
+ return NOTIFY_DONE;
+ }
+
+-struct dsa_switchdev_event_work {
+- struct work_struct work;
+- struct switchdev_notifier_fdb_info fdb_info;
+- struct net_device *dev;
+- unsigned long event;
+-};
++static void
++dsa_fdb_offload_notify(struct dsa_switchdev_event_work *switchdev_work)
++{
++ struct dsa_switch *ds = switchdev_work->ds;
++ struct switchdev_notifier_fdb_info info;
++ struct dsa_port *dp;
++
++ if (!dsa_is_user_port(ds, switchdev_work->port))
++ return;
++
++ info.addr = switchdev_work->addr;
++ info.vid = switchdev_work->vid;
++ info.offloaded = true;
++ dp = dsa_to_port(ds, switchdev_work->port);
++ call_switchdev_notifiers(SWITCHDEV_FDB_OFFLOADED,
++ dp->slave, &info.info, NULL);
++}
+
+ static void dsa_slave_switchdev_event_work(struct work_struct *work)
+ {
+ struct dsa_switchdev_event_work *switchdev_work =
+ container_of(work, struct dsa_switchdev_event_work, work);
+- struct net_device *dev = switchdev_work->dev;
+- struct switchdev_notifier_fdb_info *fdb_info;
+- struct dsa_port *dp = dsa_slave_to_port(dev);
++ struct dsa_switch *ds = switchdev_work->ds;
++ struct dsa_port *dp;
+ int err;
+
++ dp = dsa_to_port(ds, switchdev_work->port);
++
+ rtnl_lock();
+ switch (switchdev_work->event) {
+ case SWITCHDEV_FDB_ADD_TO_DEVICE:
+- fdb_info = &switchdev_work->fdb_info;
+- if (!fdb_info->added_by_user)
+- break;
+-
+- err = dsa_port_fdb_add(dp, fdb_info->addr, fdb_info->vid);
++ err = dsa_port_fdb_add(dp, switchdev_work->addr,
++ switchdev_work->vid);
+ if (err) {
+- netdev_err(dev,
+- "failed to add %pM vid %d to fdb: %d\n",
+- fdb_info->addr, fdb_info->vid, err);
++ dev_err(ds->dev,
++ "port %d failed to add %pM vid %d to fdb: %d\n",
++ dp->index, switchdev_work->addr,
++ switchdev_work->vid, err);
+ break;
+ }
+- fdb_info->offloaded = true;
+- call_switchdev_notifiers(SWITCHDEV_FDB_OFFLOADED, dev,
+- &fdb_info->info, NULL);
++ dsa_fdb_offload_notify(switchdev_work);
+ break;
+
+ case SWITCHDEV_FDB_DEL_TO_DEVICE:
+- fdb_info = &switchdev_work->fdb_info;
+- if (!fdb_info->added_by_user)
+- break;
+-
+- err = dsa_port_fdb_del(dp, fdb_info->addr, fdb_info->vid);
++ err = dsa_port_fdb_del(dp, switchdev_work->addr,
++ switchdev_work->vid);
+ if (err) {
+- netdev_err(dev,
+- "failed to delete %pM vid %d from fdb: %d\n",
+- fdb_info->addr, fdb_info->vid, err);
++ dev_err(ds->dev,
++ "port %d failed to delete %pM vid %d from fdb: %d\n",
++ dp->index, switchdev_work->addr,
++ switchdev_work->vid, err);
+ }
+
+ break;
+ }
+ rtnl_unlock();
+
+- kfree(switchdev_work->fdb_info.addr);
+ kfree(switchdev_work);
+- dev_put(dev);
+-}
+-
+-static int
+-dsa_slave_switchdev_fdb_work_init(struct dsa_switchdev_event_work *
+- switchdev_work,
+- const struct switchdev_notifier_fdb_info *
+- fdb_info)
+-{
+- memcpy(&switchdev_work->fdb_info, fdb_info,
+- sizeof(switchdev_work->fdb_info));
+- switchdev_work->fdb_info.addr = kzalloc(ETH_ALEN, GFP_ATOMIC);
+- if (!switchdev_work->fdb_info.addr)
+- return -ENOMEM;
+- ether_addr_copy((u8 *)switchdev_work->fdb_info.addr,
+- fdb_info->addr);
+- return 0;
++ if (dsa_is_user_port(ds, dp->index))
++ dev_put(dp->slave);
+ }
+
+ /* Called under rcu_read_lock() */
+@@ -1642,7 +1632,9 @@ static int dsa_slave_switchdev_event(str
+ unsigned long event, void *ptr)
+ {
+ struct net_device *dev = switchdev_notifier_info_to_dev(ptr);
++ const struct switchdev_notifier_fdb_info *fdb_info;
+ struct dsa_switchdev_event_work *switchdev_work;
++ struct dsa_port *dp;
+ int err;
+
+ if (event == SWITCHDEV_PORT_ATTR_SET) {
+@@ -1655,20 +1647,32 @@ static int dsa_slave_switchdev_event(str
+ if (!dsa_slave_dev_check(dev))
+ return NOTIFY_DONE;
+
++ dp = dsa_slave_to_port(dev);
++
+ switchdev_work = kzalloc(sizeof(*switchdev_work), GFP_ATOMIC);
+ if (!switchdev_work)
+ return NOTIFY_BAD;
+
+ INIT_WORK(&switchdev_work->work,
+ dsa_slave_switchdev_event_work);
+- switchdev_work->dev = dev;
++ switchdev_work->ds = dp->ds;
++ switchdev_work->port = dp->index;
+ switchdev_work->event = event;
+
+ switch (event) {
+ case SWITCHDEV_FDB_ADD_TO_DEVICE: /* fall through */
+ case SWITCHDEV_FDB_DEL_TO_DEVICE:
+- if (dsa_slave_switchdev_fdb_work_init(switchdev_work, ptr))
+- goto err_fdb_work_init;
++ fdb_info = ptr;
++
++ if (!fdb_info->added_by_user) {
++ kfree(switchdev_work);
++ return NOTIFY_OK;
++ }
++
++ ether_addr_copy(switchdev_work->addr,
++ fdb_info->addr);
++ switchdev_work->vid = fdb_info->vid;
++
+ dev_hold(dev);
+ break;
+ default:
+@@ -1678,10 +1682,6 @@ static int dsa_slave_switchdev_event(str
+
+ dsa_schedule_work(&switchdev_work->work);
+ return NOTIFY_OK;
+-
+-err_fdb_work_init:
+- kfree(switchdev_work);
+- return NOTIFY_BAD;
+ }
+
+ static int dsa_slave_switchdev_blocking_event(struct notifier_block *unused,
--- /dev/null
+From 447d290a58bd335d68f665713842365d3d6447df Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:33 +0200
+Subject: [PATCH] net: dsa: move switchdev event implementation under the same
+ switch/case statement
+
+We'll need to start listening to SWITCHDEV_FDB_{ADD,DEL}_TO_DEVICE
+events even for interfaces where dsa_slave_dev_check returns false, so
+we need that check inside the switch-case statement for SWITCHDEV_FDB_*.
+
+This movement also avoids a useless allocation / free of switchdev_work
+on the untreated "default event" case.
+
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ net/dsa/slave.c | 35 ++++++++++++++++-------------------
+ 1 file changed, 16 insertions(+), 19 deletions(-)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1637,31 +1637,29 @@ static int dsa_slave_switchdev_event(str
+ struct dsa_port *dp;
+ int err;
+
+- if (event == SWITCHDEV_PORT_ATTR_SET) {
++ switch (event) {
++ case SWITCHDEV_PORT_ATTR_SET:
+ err = switchdev_handle_port_attr_set(dev, ptr,
+ dsa_slave_dev_check,
+ dsa_slave_port_attr_set);
+ return notifier_from_errno(err);
+- }
+-
+- if (!dsa_slave_dev_check(dev))
+- return NOTIFY_DONE;
++ case SWITCHDEV_FDB_ADD_TO_DEVICE:
++ case SWITCHDEV_FDB_DEL_TO_DEVICE:
++ if (!dsa_slave_dev_check(dev))
++ return NOTIFY_DONE;
+
+- dp = dsa_slave_to_port(dev);
++ dp = dsa_slave_to_port(dev);
+
+- switchdev_work = kzalloc(sizeof(*switchdev_work), GFP_ATOMIC);
+- if (!switchdev_work)
+- return NOTIFY_BAD;
+-
+- INIT_WORK(&switchdev_work->work,
+- dsa_slave_switchdev_event_work);
+- switchdev_work->ds = dp->ds;
+- switchdev_work->port = dp->index;
+- switchdev_work->event = event;
++ switchdev_work = kzalloc(sizeof(*switchdev_work), GFP_ATOMIC);
++ if (!switchdev_work)
++ return NOTIFY_BAD;
++
++ INIT_WORK(&switchdev_work->work,
++ dsa_slave_switchdev_event_work);
++ switchdev_work->ds = dp->ds;
++ switchdev_work->port = dp->index;
++ switchdev_work->event = event;
+
+- switch (event) {
+- case SWITCHDEV_FDB_ADD_TO_DEVICE: /* fall through */
+- case SWITCHDEV_FDB_DEL_TO_DEVICE:
+ fdb_info = ptr;
+
+ if (!fdb_info->added_by_user) {
+@@ -1674,13 +1672,12 @@ static int dsa_slave_switchdev_event(str
+ switchdev_work->vid = fdb_info->vid;
+
+ dev_hold(dev);
++ dsa_schedule_work(&switchdev_work->work);
+ break;
+ default:
+- kfree(switchdev_work);
+ return NOTIFY_DONE;
+ }
+
+- dsa_schedule_work(&switchdev_work->work);
+ return NOTIFY_OK;
+ }
+
--- /dev/null
+From 5fb4a451a87d8ed3363d28b63a3295399373d6c4 Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:34 +0200
+Subject: [PATCH] net: dsa: exit early in dsa_slave_switchdev_event if we can't
+ program the FDB
+
+Right now, the following would happen for a switch driver that does not
+implement .port_fdb_add or .port_fdb_del.
+
+dsa_slave_switchdev_event returns NOTIFY_OK and schedules:
+-> dsa_slave_switchdev_event_work
+ -> dsa_port_fdb_add
+ -> dsa_port_notify(DSA_NOTIFIER_FDB_ADD)
+ -> dsa_switch_fdb_add
+ -> if (!ds->ops->port_fdb_add) return -EOPNOTSUPP;
+ -> an error is printed with dev_dbg, and
+ dsa_fdb_offload_notify(switchdev_work) is not called.
+
+We can avoid scheduling the worker for nothing and say NOTIFY_DONE.
+Because we don't call dsa_fdb_offload_notify, the static FDB entry will
+remain just in the software bridge.
+
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+---
+ net/dsa/slave.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1650,6 +1650,9 @@ static int dsa_slave_switchdev_event(str
+
+ dp = dsa_slave_to_port(dev);
+
++ if (!dp->ds->ops->port_fdb_add || !dp->ds->ops->port_fdb_del)
++ return NOTIFY_DONE;
++
+ switchdev_work = kzalloc(sizeof(*switchdev_work), GFP_ATOMIC);
+ if (!switchdev_work)
+ return NOTIFY_BAD;
--- /dev/null
+From d5f19486cee79d04c054427577ac96ed123706db Mon Sep 17 00:00:00 2001
+From: Vladimir Oltean <vladimir.oltean@nxp.com>
+Date: Wed, 6 Jan 2021 11:51:35 +0200
+Subject: [PATCH] net: dsa: listen for SWITCHDEV_{FDB,DEL}_ADD_TO_DEVICE on
+ foreign bridge neighbors
+
+Some DSA switches (and not only) cannot learn source MAC addresses from
+packets injected from the CPU. They only perform hardware address
+learning from inbound traffic.
+
+This can be problematic when we have a bridge spanning some DSA switch
+ports and some non-DSA ports (which we'll call "foreign interfaces" from
+DSA's perspective).
+
+There are 2 classes of problems created by the lack of learning on
+CPU-injected traffic:
+- excessive flooding, due to the fact that DSA treats those addresses as
+ unknown
+- the risk of stale routes, which can lead to temporary packet loss
+
+To illustrate the second class, consider the following situation, which
+is common in production equipment (wireless access points, where there
+is a WLAN interface and an Ethernet switch, and these form a single
+bridging domain).
+
+ AP 1:
+ +------------------------------------------------------------------------+
+ | br0 |
+ +------------------------------------------------------------------------+
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | swp0 | | swp1 | | swp2 | | swp3 | | wlan0 |
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | ^ ^
+ | | |
+ | | |
+ | Client A Client B
+ |
+ |
+ |
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | swp0 | | swp1 | | swp2 | | swp3 | | wlan0 |
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ +------------------------------------------------------------------------+
+ | br0 |
+ +------------------------------------------------------------------------+
+ AP 2
+
+- br0 of AP 1 will know that Clients A and B are reachable via wlan0
+- the hardware fdb of a DSA switch driver today is not kept in sync with
+ the software entries on other bridge ports, so it will not know that
+ clients A and B are reachable via the CPU port UNLESS the hardware
+ switch itself performs SA learning from traffic injected from the CPU.
+ Nonetheless, a substantial number of switches don't.
+- the hardware fdb of the DSA switch on AP 2 may autonomously learn that
+ Client A and B are reachable through swp0. Therefore, the software br0
+ of AP 2 also may or may not learn this. In the example we're
+ illustrating, some Ethernet traffic has been going on, and br0 from AP
+ 2 has indeed learnt that it can reach Client B through swp0.
+
+One of the wireless clients, say Client B, disconnects from AP 1 and
+roams to AP 2. The topology now looks like this:
+
+ AP 1:
+ +------------------------------------------------------------------------+
+ | br0 |
+ +------------------------------------------------------------------------+
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | swp0 | | swp1 | | swp2 | | swp3 | | wlan0 |
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | ^
+ | |
+ | Client A
+ |
+ |
+ | Client B
+ | |
+ | v
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ | swp0 | | swp1 | | swp2 | | swp3 | | wlan0 |
+ +------------+ +------------+ +------------+ +------------+ +------------+
+ +------------------------------------------------------------------------+
+ | br0 |
+ +------------------------------------------------------------------------+
+ AP 2
+
+- br0 of AP 1 still knows that Client A is reachable via wlan0 (no change)
+- br0 of AP 1 will (possibly) know that Client B has left wlan0. There
+ are cases where it might never find out though. Either way, DSA today
+ does not process that notification in any way.
+- the hardware FDB of the DSA switch on AP 1 may learn autonomously that
+ Client B can be reached via swp0, if it receives any packet with
+ Client 1's source MAC address over Ethernet.
+- the hardware FDB of the DSA switch on AP 2 still thinks that Client B
+ can be reached via swp0. It does not know that it has roamed to wlan0,
+ because it doesn't perform SA learning from the CPU port.
+
+Now Client A contacts Client B.
+AP 1 routes the packet fine towards swp0 and delivers it on the Ethernet
+segment.
+AP 2 sees a frame on swp0 and its fdb says that the destination is swp0.
+Hairpinning is disabled => drop.
+
+This problem comes from the fact that these switches have a 'blind spot'
+for addresses coming from software bridging. The generic solution is not
+to assume that hardware learning can be enabled somehow, but to listen
+to more bridge learning events. It turns out that the bridge driver does
+learn in software from all inbound frames, in __br_handle_local_finish.
+A proper SWITCHDEV_FDB_ADD_TO_DEVICE notification is emitted for the
+addresses serviced by the bridge on 'foreign' interfaces. The software
+bridge also does the right thing on migration, by notifying that the old
+entry is deleted, so that does not need to be special-cased in DSA. When
+it is deleted, we just need to delete our static FDB entry towards the
+CPU too, and wait.
+
+The problem is that DSA currently only cares about SWITCHDEV_FDB_ADD_TO_DEVICE
+events received on its own interfaces, such as static FDB entries.
+
+Luckily we can change that, and DSA can listen to all switchdev FDB
+add/del events in the system and figure out if those events were emitted
+by a bridge that spans at least one of DSA's own ports. In case that is
+true, DSA will also offload that address towards its own CPU port, in
+the eventuality that there might be bridge clients attached to the DSA
+switch who want to talk to the station connected to the foreign
+interface.
+
+In terms of implementation, we need to keep the fdb_info->added_by_user
+check for the case where the switchdev event was targeted directly at a
+DSA switch port. But we don't need to look at that flag for snooped
+events. So the check is currently too late, we need to move it earlier.
+This also simplifies the code a bit, since we avoid uselessly allocating
+and freeing switchdev_work.
+
+We could probably do some improvements in the future. For example,
+multi-bridge support is rudimentary at the moment. If there are two
+bridges spanning a DSA switch's ports, and both of them need to service
+the same MAC address, then what will happen is that the migration of one
+of those stations will trigger the deletion of the FDB entry from the
+CPU port while it is still used by other bridge. That could be improved
+with reference counting but is left for another time.
+
+This behavior needs to be enabled at driver level by setting
+ds->assisted_learning_on_cpu_port = true. This is because we don't want
+to inflict a potential performance penalty (accesses through
+MDIO/I2C/SPI are expensive) to hardware that really doesn't need it
+because address learning on the CPU port works there.
+
+Reported-by: DENG Qingfang <dqfext@gmail.com>
+Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
+Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
+Reviewed-by: Andrew Lunn <andrew@lunn.ch>
+Signed-off-by: Jakub Kicinski <kuba@kernel.org>
+[Backported to linux-5.4.y]
+Signed-off-by: DENG Qingfang <dqfext@gmail.com>
+---
+ include/net/dsa.h | 5 ++++
+ net/dsa/slave.c | 63 ++++++++++++++++++++++++++++++++++++++---------
+ 2 files changed, 57 insertions(+), 11 deletions(-)
+
+--- a/include/net/dsa.h
++++ b/include/net/dsa.h
+@@ -279,6 +279,11 @@ struct dsa_switch {
+ */
+ bool configure_vlan_while_not_filtering;
+
++ /* Let DSA manage the FDB entries towards the CPU, based on the
++ * software bridge database.
++ */
++ bool assisted_learning_on_cpu_port;
++
+ /* In case vlan_filtering_is_global is set, the VLAN awareness state
+ * should be retrieved from here and not from the per-port settings.
+ */
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1627,6 +1627,25 @@ static void dsa_slave_switchdev_event_wo
+ dev_put(dp->slave);
+ }
+
++static int dsa_lower_dev_walk(struct net_device *lower_dev, void *data)
++{
++ if (dsa_slave_dev_check(lower_dev)) {
++ *((void **)data) = (void *)netdev_priv(lower_dev);
++ return 1;
++ }
++
++ return 0;
++}
++
++static struct dsa_slave_priv *dsa_slave_dev_lower_find(struct net_device *dev)
++{
++ struct dsa_slave_priv *data = NULL;
++
++ netdev_walk_all_lower_dev_rcu(dev, dsa_lower_dev_walk, (void **) &data);
++
++ return data;
++}
++
+ /* Called under rcu_read_lock() */
+ static int dsa_slave_switchdev_event(struct notifier_block *unused,
+ unsigned long event, void *ptr)
+@@ -1645,10 +1664,37 @@ static int dsa_slave_switchdev_event(str
+ return notifier_from_errno(err);
+ case SWITCHDEV_FDB_ADD_TO_DEVICE:
+ case SWITCHDEV_FDB_DEL_TO_DEVICE:
+- if (!dsa_slave_dev_check(dev))
+- return NOTIFY_DONE;
++ fdb_info = ptr;
++
++ if (dsa_slave_dev_check(dev)) {
++ if (!fdb_info->added_by_user)
++ return NOTIFY_OK;
++
++ dp = dsa_slave_to_port(dev);
++ } else {
++ /* Snoop addresses learnt on foreign interfaces
++ * bridged with us, for switches that don't
++ * automatically learn SA from CPU-injected traffic
++ */
++ struct net_device *br_dev;
++ struct dsa_slave_priv *p;
++
++ br_dev = netdev_master_upper_dev_get_rcu(dev);
++ if (!br_dev)
++ return NOTIFY_DONE;
++
++ if (!netif_is_bridge_master(br_dev))
++ return NOTIFY_DONE;
++
++ p = dsa_slave_dev_lower_find(br_dev);
++ if (!p)
++ return NOTIFY_DONE;
+
+- dp = dsa_slave_to_port(dev);
++ dp = p->dp->cpu_dp;
++
++ if (!dp->ds->assisted_learning_on_cpu_port)
++ return NOTIFY_DONE;
++ }
+
+ if (!dp->ds->ops->port_fdb_add || !dp->ds->ops->port_fdb_del)
+ return NOTIFY_DONE;
+@@ -1663,18 +1709,13 @@ static int dsa_slave_switchdev_event(str
+ switchdev_work->port = dp->index;
+ switchdev_work->event = event;
+
+- fdb_info = ptr;
+-
+- if (!fdb_info->added_by_user) {
+- kfree(switchdev_work);
+- return NOTIFY_OK;
+- }
+-
+ ether_addr_copy(switchdev_work->addr,
+ fdb_info->addr);
+ switchdev_work->vid = fdb_info->vid;
+
+- dev_hold(dev);
++ /* Hold a reference on the slave for dsa_fdb_offload_notify */
++ if (dsa_is_user_port(dp->ds, dp->index))
++ dev_hold(dev);
+ dsa_schedule_work(&switchdev_work->work);
+ break;
+ default:
--- /dev/null
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -1930,6 +1930,7 @@ static int mv88e6xxx_port_fdb_add(struct
+ struct mv88e6xxx_chip *chip = ds->priv;
+ int err;
+
++ vid = vid ? : 1;
+ mv88e6xxx_reg_lock(chip);
+ err = mv88e6xxx_port_db_load_purge(chip, port, addr, vid,
+ MV88E6XXX_G1_ATU_DATA_STATE_UC_STATIC);
+@@ -1944,6 +1945,7 @@ static int mv88e6xxx_port_fdb_del(struct
+ struct mv88e6xxx_chip *chip = ds->priv;
+ int err;
+
++ vid = vid ? : 1;
+ mv88e6xxx_reg_lock(chip);
+ err = mv88e6xxx_port_db_load_purge(chip, port, addr, vid, 0);
+ mv88e6xxx_reg_unlock(chip);
--- /dev/null
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -2492,6 +2492,9 @@ static int mv88e6xxx_setup_port(struct m
+ if (dsa_is_cpu_port(ds, port))
+ reg = 0;
+
++ /* Disable ATU member violation interrupt */
++ reg |= MV88E6XXX_PORT_ASSOC_VECTOR_IGNORE_WRONG;
++
+ err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_ASSOC_VECTOR,
+ reg);
+ if (err)
--- /dev/null
+From 46fe6cecb296d850c1ee2b333e57093ac4b733f3 Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:09 +0100
+Subject: [PATCH] net: bridge: switchdev: Refactor br_switchdev_fdb_notify
+
+Instead of having to add more and more arguments to
+br_switchdev_fdb_call_notifiers, get rid of it and build the info
+struct directly in br_switchdev_fdb_notify.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+Reviewed-by: Vladimir Oltean <olteanv@gmail.com>
+---
+ net/bridge/br_switchdev.c | 37 +++++++++++--------------------------
+ 1 file changed, 11 insertions(+), 26 deletions(-)
+
+--- a/net/bridge/br_switchdev.c
++++ b/net/bridge/br_switchdev.c
+@@ -102,42 +102,27 @@ int br_switchdev_set_port_flag(struct ne
+ return 0;
+ }
+
+-static void
+-br_switchdev_fdb_call_notifiers(bool adding, const unsigned char *mac,
+- u16 vid, struct net_device *dev,
+- bool added_by_user, bool offloaded)
+-{
+- struct switchdev_notifier_fdb_info info;
+- unsigned long notifier_type;
+-
+- info.addr = mac;
+- info.vid = vid;
+- info.added_by_user = added_by_user;
+- info.offloaded = offloaded;
+- notifier_type = adding ? SWITCHDEV_FDB_ADD_TO_DEVICE : SWITCHDEV_FDB_DEL_TO_DEVICE;
+- call_switchdev_notifiers(notifier_type, dev, &info.info, NULL);
+-}
+-
+ void
+ br_switchdev_fdb_notify(const struct net_bridge_fdb_entry *fdb, int type)
+ {
++ struct switchdev_notifier_fdb_info info = {
++ .addr = fdb->key.addr.addr,
++ .vid = fdb->key.vlan_id,
++ .added_by_user = fdb->added_by_user,
++ .offloaded = fdb->offloaded,
++ };
++
+ if (!fdb->dst)
+ return;
+
+ switch (type) {
+ case RTM_DELNEIGH:
+- br_switchdev_fdb_call_notifiers(false, fdb->key.addr.addr,
+- fdb->key.vlan_id,
+- fdb->dst->dev,
+- fdb->added_by_user,
+- fdb->offloaded);
++ call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_DEVICE,
++ fdb->dst->dev, &info.info, NULL);
+ break;
+ case RTM_NEWNEIGH:
+- br_switchdev_fdb_call_notifiers(true, fdb->key.addr.addr,
+- fdb->key.vlan_id,
+- fdb->dst->dev,
+- fdb->added_by_user,
+- fdb->offloaded);
++ call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_DEVICE,
++ fdb->dst->dev, &info.info, NULL);
+ break;
+ }
+ }
--- /dev/null
+From ec5be4f79026282925ae383caa431a8d41e3456a Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:10 +0100
+Subject: [PATCH] net: bridge: switchdev: Include local flag in FDB
+ notifications
+
+Some switchdev drivers, notably DSA, ignore all dynamically learned
+address notifications (!added_by_user) as these are autonomously added
+by the switch. Previously, such a notification was indistinguishable
+from a local address notification. Include a local bit in the
+notification so that the two classes can be discriminated.
+
+This allows DSA-like devices to add local addresses to the hardware
+FDB (with the CPU as the destination), thereby avoiding flows towards
+the CPU being flooded by the switch as unknown unicast.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ include/net/switchdev.h | 1 +
+ net/bridge/br_switchdev.c | 1 +
+ 2 files changed, 2 insertions(+)
+
+--- a/include/net/switchdev.h
++++ b/include/net/switchdev.h
+@@ -124,6 +124,7 @@ struct switchdev_notifier_fdb_info {
+ const unsigned char *addr;
+ u16 vid;
+ u8 added_by_user:1,
++ local:1,
+ offloaded:1;
+ };
+
+--- a/net/bridge/br_switchdev.c
++++ b/net/bridge/br_switchdev.c
+@@ -109,6 +109,7 @@ br_switchdev_fdb_notify(const struct net
+ .addr = fdb->key.addr.addr,
+ .vid = fdb->key.vlan_id,
+ .added_by_user = fdb->added_by_user,
++ .local = fdb->is_local,
+ .offloaded = fdb->offloaded,
+ };
+
--- /dev/null
+From 2e50fd9322047253c327550b4485cf8761035a8c Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:11 +0100
+Subject: [PATCH] net: bridge: switchdev: Send FDB notifications for host
+ addresses
+
+Treat addresses added to the bridge itself in the same way as regular
+ports and send out a notification so that drivers may sync it down to
+the hardware FDB.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ net/bridge/br_fdb.c | 4 ++--
+ net/bridge/br_private.h | 7 ++++---
+ net/bridge/br_switchdev.c | 11 +++++------
+ 3 files changed, 11 insertions(+), 11 deletions(-)
+
+--- a/net/bridge/br_fdb.c
++++ b/net/bridge/br_fdb.c
+@@ -581,7 +581,7 @@ void br_fdb_update(struct net_bridge *br
+
+ /* fastpath: update of existing entry */
+ if (unlikely(source != fdb->dst && !fdb->is_sticky)) {
+- br_switchdev_fdb_notify(fdb, RTM_DELNEIGH);
++ br_switchdev_fdb_notify(br, fdb, RTM_DELNEIGH);
+ fdb->dst = source;
+ fdb_modified = true;
+ /* Take over HW learned entry */
+@@ -697,7 +697,7 @@ static void fdb_notify(struct net_bridge
+ int err = -ENOBUFS;
+
+ if (swdev_notify)
+- br_switchdev_fdb_notify(fdb, type);
++ br_switchdev_fdb_notify(br, fdb, type);
+
+ skb = nlmsg_new(fdb_nlmsg_size(), GFP_ATOMIC);
+ if (skb == NULL)
+--- a/net/bridge/br_private.h
++++ b/net/bridge/br_private.h
+@@ -1203,8 +1203,8 @@ bool nbp_switchdev_allowed_egress(const
+ int br_switchdev_set_port_flag(struct net_bridge_port *p,
+ unsigned long flags,
+ unsigned long mask);
+-void br_switchdev_fdb_notify(const struct net_bridge_fdb_entry *fdb,
+- int type);
++void br_switchdev_fdb_notify(struct net_bridge *br,
++ const struct net_bridge_fdb_entry *fdb, int type);
+ int br_switchdev_port_vlan_add(struct net_device *dev, u16 vid, u16 flags,
+ struct netlink_ext_ack *extack);
+ int br_switchdev_port_vlan_del(struct net_device *dev, u16 vid);
+@@ -1250,7 +1250,8 @@ static inline int br_switchdev_port_vlan
+ }
+
+ static inline void
+-br_switchdev_fdb_notify(const struct net_bridge_fdb_entry *fdb, int type)
++br_switchdev_fdb_notify(struct net_bridge *br,
++ const struct net_bridge_fdb_entry *fdb, int type)
+ {
+ }
+
+--- a/net/bridge/br_switchdev.c
++++ b/net/bridge/br_switchdev.c
+@@ -103,7 +103,8 @@ int br_switchdev_set_port_flag(struct ne
+ }
+
+ void
+-br_switchdev_fdb_notify(const struct net_bridge_fdb_entry *fdb, int type)
++br_switchdev_fdb_notify(struct net_bridge *br,
++ const struct net_bridge_fdb_entry *fdb, int type)
+ {
+ struct switchdev_notifier_fdb_info info = {
+ .addr = fdb->key.addr.addr,
+@@ -112,18 +113,16 @@ br_switchdev_fdb_notify(const struct net
+ .local = fdb->is_local,
+ .offloaded = fdb->offloaded,
+ };
+-
+- if (!fdb->dst)
+- return;
++ struct net_device *dev = fdb->dst ? fdb->dst->dev : br->dev;
+
+ switch (type) {
+ case RTM_DELNEIGH:
+ call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_DEVICE,
+- fdb->dst->dev, &info.info, NULL);
++ dev, &info.info, NULL);
+ break;
+ case RTM_NEWNEIGH:
+ call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_DEVICE,
+- fdb->dst->dev, &info.info, NULL);
++ dev, &info.info, NULL);
+ break;
+ }
+ }
--- /dev/null
+From dd082716b43a3684b2f473ae5d1e76d1c076d86d Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:12 +0100
+Subject: [PATCH] net: dsa: Include local addresses in assisted CPU port
+ learning
+
+Add local addresses (i.e. the ports' MAC addresses) to the hardware
+FDB when assisted CPU port learning is enabled.
+
+NOTE: The bridge's own MAC address is also "local". If that address is
+not shared with any port, the bridge's MAC is not be added by this
+functionality - but the following commit takes care of that case.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ net/dsa/slave.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1695,10 +1695,12 @@ static int dsa_slave_switchdev_event(str
+ fdb_info = ptr;
+
+ if (dsa_slave_dev_check(dev)) {
+- if (!fdb_info->added_by_user)
+- return NOTIFY_OK;
+-
+ dp = dsa_slave_to_port(dev);
++
++ if (fdb_info->local && dp->ds->assisted_learning_on_cpu_port)
++ dp = dp->cpu_dp;
++ else if (!fdb_info->added_by_user)
++ return NOTIFY_OK;
+ } else {
+ /* Snoop addresses learnt on foreign interfaces
+ * bridged with us, for switches that don't
--- /dev/null
+From 0663ebde114a6fb2c28c622ba5212b302d4d2581 Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:13 +0100
+Subject: [PATCH] net: dsa: Include bridge addresses in assisted CPU port
+ learning
+
+Now that notifications are sent out for addresses added to the bridge
+itself, extend DSA to include those addresses in the hardware FDB when
+assisted CPU port learning is enabled.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ net/dsa/slave.c | 6 +++++-
+ 1 file changed, 5 insertions(+), 1 deletion(-)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1709,7 +1709,11 @@ static int dsa_slave_switchdev_event(str
+ struct net_device *br_dev;
+ struct dsa_slave_priv *p;
+
+- br_dev = netdev_master_upper_dev_get_rcu(dev);
++ if (netif_is_bridge_master(dev))
++ br_dev = dev;
++ else
++ br_dev = netdev_master_upper_dev_get_rcu(dev);
++
+ if (!br_dev)
+ return NOTIFY_DONE;
+
--- /dev/null
+From 81e39fd78db82fb51b05fff309b9c521f1a0bc5a Mon Sep 17 00:00:00 2001
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Date: Sat, 16 Jan 2021 02:25:14 +0100
+Subject: [PATCH] net: dsa: Sync static FDB entries on foreign interfaces to
+ hardware
+
+Reuse the "assisted_learning_on_cpu_port" functionality to always add
+entries for user-configured entries on foreign interfaces, even if
+assisted_learning_on_cpu_port is not enabled. E.g. in this situation:
+
+ br0
+ / \
+swp0 dummy0
+
+$ bridge fdb add 02:00:de:ad:00:01 dev dummy0 vlan 1 master
+
+Results in DSA adding an entry in the hardware FDB, pointing this
+address towards the CPU port.
+
+The same is true for entries added to the bridge itself, e.g:
+
+$ bridge fdb add 02:00:de:ad:00:01 dev br0 vlan 1 self
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ net/dsa/slave.c | 12 ++++++++----
+ 1 file changed, 8 insertions(+), 4 deletions(-)
+
+--- a/net/dsa/slave.c
++++ b/net/dsa/slave.c
+@@ -1702,9 +1702,12 @@ static int dsa_slave_switchdev_event(str
+ else if (!fdb_info->added_by_user)
+ return NOTIFY_OK;
+ } else {
+- /* Snoop addresses learnt on foreign interfaces
+- * bridged with us, for switches that don't
+- * automatically learn SA from CPU-injected traffic
++ /* Snoop addresses added to foreign interfaces
++ * bridged with us, or the bridge
++ * itself. Dynamically learned addresses can
++ * also be added for switches that don't
++ * automatically learn SA from CPU-injected
++ * traffic.
+ */
+ struct net_device *br_dev;
+ struct dsa_slave_priv *p;
+@@ -1726,7 +1729,8 @@ static int dsa_slave_switchdev_event(str
+
+ dp = p->dp->cpu_dp;
+
+- if (!dp->ds->assisted_learning_on_cpu_port)
++ if (!fdb_info->added_by_user &&
++ !dp->ds->assisted_learning_on_cpu_port)
+ return NOTIFY_DONE;
+ }
+
--- /dev/null
+From: Tobias Waldekranz <tobias@waldekranz.com>
+Subject: [RFC net-next 7/7] net: dsa: mv88e6xxx: Request assisted learning on CPU port
+Date: Sat, 16 Jan 2021 02:25:15 +0100
+Archived-At: <https://lore.kernel.org/netdev/20210116012515.3152-8-tobias@waldekranz.com/>
+
+While the hardware is capable of performing learning on the CPU port,
+it requires alot of additions to the bridge's forwarding path in order
+to handle multi-destination traffic correctly.
+
+Until that is in place, opt for the next best thing and let DSA sync
+the relevant addresses down to the hardware FDB.
+
+Signed-off-by: Tobias Waldekranz <tobias@waldekranz.com>
+---
+ drivers/net/dsa/mv88e6xxx/chip.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+--- a/drivers/net/dsa/mv88e6xxx/chip.c
++++ b/drivers/net/dsa/mv88e6xxx/chip.c
+@@ -5080,6 +5080,7 @@ static int mv88e6xxx_register_switch(str
+ ds->ops = &mv88e6xxx_switch_ops;
+ ds->ageing_time_min = chip->info->age_time_coeff;
+ ds->ageing_time_max = chip->info->age_time_coeff * U8_MAX;
++ ds->assisted_learning_on_cpu_port = true;
+
+ dev_set_drvdata(dev, ds);
+