mirror of
https://github.com/torvalds/linux.git
synced 2024-12-01 00:21:32 +00:00
7e580490ac
The Felix driver declares FDB isolation but puts all standalone ports in
VID 0. This is mostly problem-free as discussed with Alvin here:
https://patchwork.kernel.org/project/netdevbpf/cover/20220302191417.1288145-1-vladimir.oltean@nxp.com/#24763870
however there is one catch. DSA still thinks that FDB entries are
installed on the CPU port as many times as there are user ports, and
this is problematic when multiple user ports share the same MAC address.
Consider the default case where all user ports inherit their MAC address
from the DSA master, and then the user runs:
ip link set swp0 address 00:01:02:03:04:05
The above will make dsa_slave_set_mac_address() call
dsa_port_standalone_host_fdb_add() for 00:01:02:03:04:05 in port 0's
standalone database, and dsa_port_standalone_host_fdb_del() for the old
address of swp0, again in swp0's standalone database.
Both the ->port_fdb_add() and ->port_fdb_del() will be propagated down
to the felix driver, which will end up deleting the old MAC address from
the CPU port. But this is still in use by other user ports, so we end up
breaking unicast termination for them.
There isn't a problem in the fact that DSA keeps track of host
standalone addresses in the individual database of each user port: some
drivers like sja1105 need this. There also isn't a problem in the fact
that some drivers choose the same VID/FID for all standalone ports.
It is just that the deletion of these host addresses must be delayed
until they are known to not be in use any longer, and only the driver
has this knowledge. Since DSA keeps these addresses in &cpu_dp->fdbs and
&cpu_db->mdbs, it is just a matter of walking over those lists and see
whether the same MAC address is present on the CPU port in the port db
of another user port.
I have considered reusing the generic dsa_port_walk_fdbs() and
dsa_port_walk_mdbs() schemes for this, but locking makes it difficult.
In the ->port_fdb_add() method and co, &dp->addr_lists_lock is held, but
dsa_port_walk_fdbs() also acquires that lock. Also, even assuming that
we introduce an unlocked variant of the address iterator, we'd still
need some relatively complex data structures, and a void *ctx in the
dsa_fdb_walk_cb_t which we don't currently pass, such that drivers are
able to figure out, after iterating, whether the same MAC address is or
isn't present in the port db of another port.
All the above, plus the fact that I expect other drivers to follow the
same model as felix where all standalone ports use the same FID, made me
conclude that a generic method provided by DSA is necessary:
dsa_fdb_present_in_other_db() and the mdb equivalent. Felix calls this
from the ->port_fdb_del() handler for the CPU port, when the database
was classified to either a port db, or a LAG db.
For symmetry, we also call this from ->port_fdb_add(), because if the
address was installed once, then installing it a second time serves no
purpose: it's already in hardware in VID 0 and it affects all standalone
ports.
This change moves dsa_db_equal() from switch.c to dsa.c, since it now
has one more caller.
Fixes: 54c3198460
("net: mscc: ocelot: enforce FDB isolation when VLAN-unaware")
Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
Signed-off-by: David S. Miller <davem@davemloft.net>
1110 lines
25 KiB
C
1110 lines
25 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Handling of a single switch chip, part of a switch fabric
|
|
*
|
|
* Copyright (c) 2017 Savoir-faire Linux Inc.
|
|
* Vivien Didelot <vivien.didelot@savoirfairelinux.com>
|
|
*/
|
|
|
|
#include <linux/if_bridge.h>
|
|
#include <linux/netdevice.h>
|
|
#include <linux/notifier.h>
|
|
#include <linux/if_vlan.h>
|
|
#include <net/switchdev.h>
|
|
|
|
#include "dsa_priv.h"
|
|
|
|
static unsigned int dsa_switch_fastest_ageing_time(struct dsa_switch *ds,
|
|
unsigned int ageing_time)
|
|
{
|
|
struct dsa_port *dp;
|
|
|
|
dsa_switch_for_each_port(dp, ds)
|
|
if (dp->ageing_time && dp->ageing_time < ageing_time)
|
|
ageing_time = dp->ageing_time;
|
|
|
|
return ageing_time;
|
|
}
|
|
|
|
static int dsa_switch_ageing_time(struct dsa_switch *ds,
|
|
struct dsa_notifier_ageing_time_info *info)
|
|
{
|
|
unsigned int ageing_time = info->ageing_time;
|
|
|
|
if (ds->ageing_time_min && ageing_time < ds->ageing_time_min)
|
|
return -ERANGE;
|
|
|
|
if (ds->ageing_time_max && ageing_time > ds->ageing_time_max)
|
|
return -ERANGE;
|
|
|
|
/* Program the fastest ageing time in case of multiple bridges */
|
|
ageing_time = dsa_switch_fastest_ageing_time(ds, ageing_time);
|
|
|
|
if (ds->ops->set_ageing_time)
|
|
return ds->ops->set_ageing_time(ds, ageing_time);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool dsa_port_mtu_match(struct dsa_port *dp,
|
|
struct dsa_notifier_mtu_info *info)
|
|
{
|
|
if (dp->ds->index == info->sw_index && dp->index == info->port)
|
|
return true;
|
|
|
|
/* Do not propagate to other switches in the tree if the notifier was
|
|
* targeted for a single switch.
|
|
*/
|
|
if (info->targeted_match)
|
|
return false;
|
|
|
|
if (dsa_port_is_dsa(dp) || dsa_port_is_cpu(dp))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static int dsa_switch_mtu(struct dsa_switch *ds,
|
|
struct dsa_notifier_mtu_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int ret;
|
|
|
|
if (!ds->ops->port_change_mtu)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_mtu_match(dp, info)) {
|
|
ret = ds->ops->port_change_mtu(ds, dp->index,
|
|
info->mtu);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_bridge_join(struct dsa_switch *ds,
|
|
struct dsa_notifier_bridge_info *info)
|
|
{
|
|
struct dsa_switch_tree *dst = ds->dst;
|
|
int err;
|
|
|
|
if (dst->index == info->tree_index && ds->index == info->sw_index) {
|
|
if (!ds->ops->port_bridge_join)
|
|
return -EOPNOTSUPP;
|
|
|
|
err = ds->ops->port_bridge_join(ds, info->port, info->bridge,
|
|
&info->tx_fwd_offload,
|
|
info->extack);
|
|
if (err)
|
|
return err;
|
|
}
|
|
|
|
if ((dst->index != info->tree_index || ds->index != info->sw_index) &&
|
|
ds->ops->crosschip_bridge_join) {
|
|
err = ds->ops->crosschip_bridge_join(ds, info->tree_index,
|
|
info->sw_index,
|
|
info->port, info->bridge,
|
|
info->extack);
|
|
if (err)
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_sync_vlan_filtering(struct dsa_switch *ds,
|
|
struct dsa_notifier_bridge_info *info)
|
|
{
|
|
struct netlink_ext_ack extack = {0};
|
|
bool change_vlan_filtering = false;
|
|
bool vlan_filtering;
|
|
struct dsa_port *dp;
|
|
int err;
|
|
|
|
if (ds->needs_standalone_vlan_filtering &&
|
|
!br_vlan_enabled(info->bridge.dev)) {
|
|
change_vlan_filtering = true;
|
|
vlan_filtering = true;
|
|
} else if (!ds->needs_standalone_vlan_filtering &&
|
|
br_vlan_enabled(info->bridge.dev)) {
|
|
change_vlan_filtering = true;
|
|
vlan_filtering = false;
|
|
}
|
|
|
|
/* If the bridge was vlan_filtering, the bridge core doesn't trigger an
|
|
* event for changing vlan_filtering setting upon slave ports leaving
|
|
* it. That is a good thing, because that lets us handle it and also
|
|
* handle the case where the switch's vlan_filtering setting is global
|
|
* (not per port). When that happens, the correct moment to trigger the
|
|
* vlan_filtering callback is only when the last port leaves the last
|
|
* VLAN-aware bridge.
|
|
*/
|
|
if (change_vlan_filtering && ds->vlan_filtering_is_global) {
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
struct net_device *br = dsa_port_bridge_dev_get(dp);
|
|
|
|
if (br && br_vlan_enabled(br)) {
|
|
change_vlan_filtering = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (change_vlan_filtering) {
|
|
err = dsa_port_vlan_filtering(dsa_to_port(ds, info->port),
|
|
vlan_filtering, &extack);
|
|
if (extack._msg)
|
|
dev_err(ds->dev, "port %d: %s\n", info->port,
|
|
extack._msg);
|
|
if (err && err != -EOPNOTSUPP)
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_bridge_leave(struct dsa_switch *ds,
|
|
struct dsa_notifier_bridge_info *info)
|
|
{
|
|
struct dsa_switch_tree *dst = ds->dst;
|
|
int err;
|
|
|
|
if (dst->index == info->tree_index && ds->index == info->sw_index &&
|
|
ds->ops->port_bridge_leave)
|
|
ds->ops->port_bridge_leave(ds, info->port, info->bridge);
|
|
|
|
if ((dst->index != info->tree_index || ds->index != info->sw_index) &&
|
|
ds->ops->crosschip_bridge_leave)
|
|
ds->ops->crosschip_bridge_leave(ds, info->tree_index,
|
|
info->sw_index, info->port,
|
|
info->bridge);
|
|
|
|
if (ds->dst->index == info->tree_index && ds->index == info->sw_index) {
|
|
err = dsa_switch_sync_vlan_filtering(ds, info);
|
|
if (err)
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Matches for all upstream-facing ports (the CPU port and all upstream-facing
|
|
* DSA links) that sit between the targeted port on which the notifier was
|
|
* emitted and its dedicated CPU port.
|
|
*/
|
|
static bool dsa_port_host_address_match(struct dsa_port *dp,
|
|
int info_sw_index, int info_port)
|
|
{
|
|
struct dsa_port *targeted_dp, *cpu_dp;
|
|
struct dsa_switch *targeted_ds;
|
|
|
|
targeted_ds = dsa_switch_find(dp->ds->dst->index, info_sw_index);
|
|
targeted_dp = dsa_to_port(targeted_ds, info_port);
|
|
cpu_dp = targeted_dp->cpu_dp;
|
|
|
|
if (dsa_switch_is_upstream_of(dp->ds, targeted_ds))
|
|
return dp->index == dsa_towards_port(dp->ds, cpu_dp->ds->index,
|
|
cpu_dp->index);
|
|
|
|
return false;
|
|
}
|
|
|
|
static struct dsa_mac_addr *dsa_mac_addr_find(struct list_head *addr_list,
|
|
const unsigned char *addr, u16 vid,
|
|
struct dsa_db db)
|
|
{
|
|
struct dsa_mac_addr *a;
|
|
|
|
list_for_each_entry(a, addr_list, list)
|
|
if (ether_addr_equal(a->addr, addr) && a->vid == vid &&
|
|
dsa_db_equal(&a->db, &db))
|
|
return a;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int dsa_port_do_mdb_add(struct dsa_port *dp,
|
|
const struct switchdev_obj_port_mdb *mdb,
|
|
struct dsa_db db)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
struct dsa_mac_addr *a;
|
|
int port = dp->index;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_mdb_add(ds, port, mdb, db);
|
|
|
|
mutex_lock(&dp->addr_lists_lock);
|
|
|
|
a = dsa_mac_addr_find(&dp->mdbs, mdb->addr, mdb->vid, db);
|
|
if (a) {
|
|
refcount_inc(&a->refcount);
|
|
goto out;
|
|
}
|
|
|
|
a = kzalloc(sizeof(*a), GFP_KERNEL);
|
|
if (!a) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
err = ds->ops->port_mdb_add(ds, port, mdb, db);
|
|
if (err) {
|
|
kfree(a);
|
|
goto out;
|
|
}
|
|
|
|
ether_addr_copy(a->addr, mdb->addr);
|
|
a->vid = mdb->vid;
|
|
a->db = db;
|
|
refcount_set(&a->refcount, 1);
|
|
list_add_tail(&a->list, &dp->mdbs);
|
|
|
|
out:
|
|
mutex_unlock(&dp->addr_lists_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_port_do_mdb_del(struct dsa_port *dp,
|
|
const struct switchdev_obj_port_mdb *mdb,
|
|
struct dsa_db db)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
struct dsa_mac_addr *a;
|
|
int port = dp->index;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_mdb_del(ds, port, mdb, db);
|
|
|
|
mutex_lock(&dp->addr_lists_lock);
|
|
|
|
a = dsa_mac_addr_find(&dp->mdbs, mdb->addr, mdb->vid, db);
|
|
if (!a) {
|
|
err = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
if (!refcount_dec_and_test(&a->refcount))
|
|
goto out;
|
|
|
|
err = ds->ops->port_mdb_del(ds, port, mdb, db);
|
|
if (err) {
|
|
refcount_set(&a->refcount, 1);
|
|
goto out;
|
|
}
|
|
|
|
list_del(&a->list);
|
|
kfree(a);
|
|
|
|
out:
|
|
mutex_unlock(&dp->addr_lists_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_port_do_fdb_add(struct dsa_port *dp, const unsigned char *addr,
|
|
u16 vid, struct dsa_db db)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
struct dsa_mac_addr *a;
|
|
int port = dp->index;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_fdb_add(ds, port, addr, vid, db);
|
|
|
|
mutex_lock(&dp->addr_lists_lock);
|
|
|
|
a = dsa_mac_addr_find(&dp->fdbs, addr, vid, db);
|
|
if (a) {
|
|
refcount_inc(&a->refcount);
|
|
goto out;
|
|
}
|
|
|
|
a = kzalloc(sizeof(*a), GFP_KERNEL);
|
|
if (!a) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
err = ds->ops->port_fdb_add(ds, port, addr, vid, db);
|
|
if (err) {
|
|
kfree(a);
|
|
goto out;
|
|
}
|
|
|
|
ether_addr_copy(a->addr, addr);
|
|
a->vid = vid;
|
|
a->db = db;
|
|
refcount_set(&a->refcount, 1);
|
|
list_add_tail(&a->list, &dp->fdbs);
|
|
|
|
out:
|
|
mutex_unlock(&dp->addr_lists_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_port_do_fdb_del(struct dsa_port *dp, const unsigned char *addr,
|
|
u16 vid, struct dsa_db db)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
struct dsa_mac_addr *a;
|
|
int port = dp->index;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_fdb_del(ds, port, addr, vid, db);
|
|
|
|
mutex_lock(&dp->addr_lists_lock);
|
|
|
|
a = dsa_mac_addr_find(&dp->fdbs, addr, vid, db);
|
|
if (!a) {
|
|
err = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
if (!refcount_dec_and_test(&a->refcount))
|
|
goto out;
|
|
|
|
err = ds->ops->port_fdb_del(ds, port, addr, vid, db);
|
|
if (err) {
|
|
refcount_set(&a->refcount, 1);
|
|
goto out;
|
|
}
|
|
|
|
list_del(&a->list);
|
|
kfree(a);
|
|
|
|
out:
|
|
mutex_unlock(&dp->addr_lists_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_do_lag_fdb_add(struct dsa_switch *ds, struct dsa_lag *lag,
|
|
const unsigned char *addr, u16 vid,
|
|
struct dsa_db db)
|
|
{
|
|
struct dsa_mac_addr *a;
|
|
int err = 0;
|
|
|
|
mutex_lock(&lag->fdb_lock);
|
|
|
|
a = dsa_mac_addr_find(&lag->fdbs, addr, vid, db);
|
|
if (a) {
|
|
refcount_inc(&a->refcount);
|
|
goto out;
|
|
}
|
|
|
|
a = kzalloc(sizeof(*a), GFP_KERNEL);
|
|
if (!a) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
err = ds->ops->lag_fdb_add(ds, *lag, addr, vid, db);
|
|
if (err) {
|
|
kfree(a);
|
|
goto out;
|
|
}
|
|
|
|
ether_addr_copy(a->addr, addr);
|
|
a->vid = vid;
|
|
refcount_set(&a->refcount, 1);
|
|
list_add_tail(&a->list, &lag->fdbs);
|
|
|
|
out:
|
|
mutex_unlock(&lag->fdb_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_do_lag_fdb_del(struct dsa_switch *ds, struct dsa_lag *lag,
|
|
const unsigned char *addr, u16 vid,
|
|
struct dsa_db db)
|
|
{
|
|
struct dsa_mac_addr *a;
|
|
int err = 0;
|
|
|
|
mutex_lock(&lag->fdb_lock);
|
|
|
|
a = dsa_mac_addr_find(&lag->fdbs, addr, vid, db);
|
|
if (!a) {
|
|
err = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
if (!refcount_dec_and_test(&a->refcount))
|
|
goto out;
|
|
|
|
err = ds->ops->lag_fdb_del(ds, *lag, addr, vid, db);
|
|
if (err) {
|
|
refcount_set(&a->refcount, 1);
|
|
goto out;
|
|
}
|
|
|
|
list_del(&a->list);
|
|
kfree(a);
|
|
|
|
out:
|
|
mutex_unlock(&lag->fdb_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_host_fdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_fdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err = 0;
|
|
|
|
if (!ds->ops->port_fdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_address_match(dp, info->sw_index,
|
|
info->port)) {
|
|
err = dsa_port_do_fdb_add(dp, info->addr, info->vid,
|
|
info->db);
|
|
if (err)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_host_fdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_fdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err = 0;
|
|
|
|
if (!ds->ops->port_fdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_address_match(dp, info->sw_index,
|
|
info->port)) {
|
|
err = dsa_port_do_fdb_del(dp, info->addr, info->vid,
|
|
info->db);
|
|
if (err)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_fdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_fdb_info *info)
|
|
{
|
|
int port = dsa_towards_port(ds, info->sw_index, info->port);
|
|
struct dsa_port *dp = dsa_to_port(ds, port);
|
|
|
|
if (!ds->ops->port_fdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
return dsa_port_do_fdb_add(dp, info->addr, info->vid, info->db);
|
|
}
|
|
|
|
static int dsa_switch_fdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_fdb_info *info)
|
|
{
|
|
int port = dsa_towards_port(ds, info->sw_index, info->port);
|
|
struct dsa_port *dp = dsa_to_port(ds, port);
|
|
|
|
if (!ds->ops->port_fdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
return dsa_port_do_fdb_del(dp, info->addr, info->vid, info->db);
|
|
}
|
|
|
|
static int dsa_switch_lag_fdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_lag_fdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
|
|
if (!ds->ops->lag_fdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
/* Notify switch only if it has a port in this LAG */
|
|
dsa_switch_for_each_port(dp, ds)
|
|
if (dsa_port_offloads_lag(dp, info->lag))
|
|
return dsa_switch_do_lag_fdb_add(ds, info->lag,
|
|
info->addr, info->vid,
|
|
info->db);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_lag_fdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_lag_fdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
|
|
if (!ds->ops->lag_fdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
/* Notify switch only if it has a port in this LAG */
|
|
dsa_switch_for_each_port(dp, ds)
|
|
if (dsa_port_offloads_lag(dp, info->lag))
|
|
return dsa_switch_do_lag_fdb_del(ds, info->lag,
|
|
info->addr, info->vid,
|
|
info->db);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_lag_change(struct dsa_switch *ds,
|
|
struct dsa_notifier_lag_info *info)
|
|
{
|
|
if (ds->index == info->sw_index && ds->ops->port_lag_change)
|
|
return ds->ops->port_lag_change(ds, info->port);
|
|
|
|
if (ds->index != info->sw_index && ds->ops->crosschip_lag_change)
|
|
return ds->ops->crosschip_lag_change(ds, info->sw_index,
|
|
info->port);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_lag_join(struct dsa_switch *ds,
|
|
struct dsa_notifier_lag_info *info)
|
|
{
|
|
if (ds->index == info->sw_index && ds->ops->port_lag_join)
|
|
return ds->ops->port_lag_join(ds, info->port, info->lag,
|
|
info->info);
|
|
|
|
if (ds->index != info->sw_index && ds->ops->crosschip_lag_join)
|
|
return ds->ops->crosschip_lag_join(ds, info->sw_index,
|
|
info->port, info->lag,
|
|
info->info);
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static int dsa_switch_lag_leave(struct dsa_switch *ds,
|
|
struct dsa_notifier_lag_info *info)
|
|
{
|
|
if (ds->index == info->sw_index && ds->ops->port_lag_leave)
|
|
return ds->ops->port_lag_leave(ds, info->port, info->lag);
|
|
|
|
if (ds->index != info->sw_index && ds->ops->crosschip_lag_leave)
|
|
return ds->ops->crosschip_lag_leave(ds, info->sw_index,
|
|
info->port, info->lag);
|
|
|
|
return -EOPNOTSUPP;
|
|
}
|
|
|
|
static int dsa_switch_mdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
int port = dsa_towards_port(ds, info->sw_index, info->port);
|
|
struct dsa_port *dp = dsa_to_port(ds, port);
|
|
|
|
if (!ds->ops->port_mdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
return dsa_port_do_mdb_add(dp, info->mdb, info->db);
|
|
}
|
|
|
|
static int dsa_switch_mdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
int port = dsa_towards_port(ds, info->sw_index, info->port);
|
|
struct dsa_port *dp = dsa_to_port(ds, port);
|
|
|
|
if (!ds->ops->port_mdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
return dsa_port_do_mdb_del(dp, info->mdb, info->db);
|
|
}
|
|
|
|
static int dsa_switch_host_mdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err = 0;
|
|
|
|
if (!ds->ops->port_mdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_address_match(dp, info->sw_index,
|
|
info->port)) {
|
|
err = dsa_port_do_mdb_add(dp, info->mdb, info->db);
|
|
if (err)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_host_mdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err = 0;
|
|
|
|
if (!ds->ops->port_mdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_address_match(dp, info->sw_index,
|
|
info->port)) {
|
|
err = dsa_port_do_mdb_del(dp, info->mdb, info->db);
|
|
if (err)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
/* Port VLANs match on the targeted port and on all DSA ports */
|
|
static bool dsa_port_vlan_match(struct dsa_port *dp,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
if (dp->ds->index == info->sw_index && dp->index == info->port)
|
|
return true;
|
|
|
|
if (dsa_port_is_dsa(dp))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Host VLANs match on the targeted port's CPU port, and on all DSA ports
|
|
* (upstream and downstream) of that switch and its upstream switches.
|
|
*/
|
|
static bool dsa_port_host_vlan_match(struct dsa_port *dp,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
struct dsa_port *targeted_dp, *cpu_dp;
|
|
struct dsa_switch *targeted_ds;
|
|
|
|
targeted_ds = dsa_switch_find(dp->ds->dst->index, info->sw_index);
|
|
targeted_dp = dsa_to_port(targeted_ds, info->port);
|
|
cpu_dp = targeted_dp->cpu_dp;
|
|
|
|
if (dsa_switch_is_upstream_of(dp->ds, targeted_ds))
|
|
return dsa_port_is_dsa(dp) || dp == cpu_dp;
|
|
|
|
return false;
|
|
}
|
|
|
|
static struct dsa_vlan *dsa_vlan_find(struct list_head *vlan_list,
|
|
const struct switchdev_obj_port_vlan *vlan)
|
|
{
|
|
struct dsa_vlan *v;
|
|
|
|
list_for_each_entry(v, vlan_list, list)
|
|
if (v->vid == vlan->vid)
|
|
return v;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int dsa_port_do_vlan_add(struct dsa_port *dp,
|
|
const struct switchdev_obj_port_vlan *vlan,
|
|
struct netlink_ext_ack *extack)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
int port = dp->index;
|
|
struct dsa_vlan *v;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports. */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_vlan_add(ds, port, vlan, extack);
|
|
|
|
/* No need to propagate on shared ports the existing VLANs that were
|
|
* re-notified after just the flags have changed. This would cause a
|
|
* refcount bump which we need to avoid, since it unbalances the
|
|
* additions with the deletions.
|
|
*/
|
|
if (vlan->changed)
|
|
return 0;
|
|
|
|
mutex_lock(&dp->vlans_lock);
|
|
|
|
v = dsa_vlan_find(&dp->vlans, vlan);
|
|
if (v) {
|
|
refcount_inc(&v->refcount);
|
|
goto out;
|
|
}
|
|
|
|
v = kzalloc(sizeof(*v), GFP_KERNEL);
|
|
if (!v) {
|
|
err = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
err = ds->ops->port_vlan_add(ds, port, vlan, extack);
|
|
if (err) {
|
|
kfree(v);
|
|
goto out;
|
|
}
|
|
|
|
v->vid = vlan->vid;
|
|
refcount_set(&v->refcount, 1);
|
|
list_add_tail(&v->list, &dp->vlans);
|
|
|
|
out:
|
|
mutex_unlock(&dp->vlans_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_port_do_vlan_del(struct dsa_port *dp,
|
|
const struct switchdev_obj_port_vlan *vlan)
|
|
{
|
|
struct dsa_switch *ds = dp->ds;
|
|
int port = dp->index;
|
|
struct dsa_vlan *v;
|
|
int err = 0;
|
|
|
|
/* No need to bother with refcounting for user ports */
|
|
if (!(dsa_port_is_cpu(dp) || dsa_port_is_dsa(dp)))
|
|
return ds->ops->port_vlan_del(ds, port, vlan);
|
|
|
|
mutex_lock(&dp->vlans_lock);
|
|
|
|
v = dsa_vlan_find(&dp->vlans, vlan);
|
|
if (!v) {
|
|
err = -ENOENT;
|
|
goto out;
|
|
}
|
|
|
|
if (!refcount_dec_and_test(&v->refcount))
|
|
goto out;
|
|
|
|
err = ds->ops->port_vlan_del(ds, port, vlan);
|
|
if (err) {
|
|
refcount_set(&v->refcount, 1);
|
|
goto out;
|
|
}
|
|
|
|
list_del(&v->list);
|
|
kfree(v);
|
|
|
|
out:
|
|
mutex_unlock(&dp->vlans_lock);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_vlan_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err;
|
|
|
|
if (!ds->ops->port_vlan_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_vlan_match(dp, info)) {
|
|
err = dsa_port_do_vlan_add(dp, info->vlan,
|
|
info->extack);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_vlan_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err;
|
|
|
|
if (!ds->ops->port_vlan_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_vlan_match(dp, info)) {
|
|
err = dsa_port_do_vlan_del(dp, info->vlan);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_host_vlan_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err;
|
|
|
|
if (!ds->ops->port_vlan_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_vlan_match(dp, info)) {
|
|
err = dsa_port_do_vlan_add(dp, info->vlan,
|
|
info->extack);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_host_vlan_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
struct dsa_port *dp;
|
|
int err;
|
|
|
|
if (!ds->ops->port_vlan_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
dsa_switch_for_each_port(dp, ds) {
|
|
if (dsa_port_host_vlan_match(dp, info)) {
|
|
err = dsa_port_do_vlan_del(dp, info->vlan);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_change_tag_proto(struct dsa_switch *ds,
|
|
struct dsa_notifier_tag_proto_info *info)
|
|
{
|
|
const struct dsa_device_ops *tag_ops = info->tag_ops;
|
|
struct dsa_port *dp, *cpu_dp;
|
|
int err;
|
|
|
|
if (!ds->ops->change_tag_protocol)
|
|
return -EOPNOTSUPP;
|
|
|
|
ASSERT_RTNL();
|
|
|
|
dsa_switch_for_each_cpu_port(cpu_dp, ds) {
|
|
err = ds->ops->change_tag_protocol(ds, cpu_dp->index,
|
|
tag_ops->proto);
|
|
if (err)
|
|
return err;
|
|
|
|
dsa_port_set_tag_protocol(cpu_dp, tag_ops);
|
|
}
|
|
|
|
/* Now that changing the tag protocol can no longer fail, let's update
|
|
* the remaining bits which are "duplicated for faster access", and the
|
|
* bits that depend on the tagger, such as the MTU.
|
|
*/
|
|
dsa_switch_for_each_user_port(dp, ds) {
|
|
struct net_device *slave = dp->slave;
|
|
|
|
dsa_slave_setup_tagger(slave);
|
|
|
|
/* rtnl_mutex is held in dsa_tree_change_tag_proto */
|
|
dsa_slave_change_mtu(slave, slave->mtu);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* We use the same cross-chip notifiers to inform both the tagger side, as well
|
|
* as the switch side, of connection and disconnection events.
|
|
* Since ds->tagger_data is owned by the tagger, it isn't a hard error if the
|
|
* switch side doesn't support connecting to this tagger, and therefore, the
|
|
* fact that we don't disconnect the tagger side doesn't constitute a memory
|
|
* leak: the tagger will still operate with persistent per-switch memory, just
|
|
* with the switch side unconnected to it. What does constitute a hard error is
|
|
* when the switch side supports connecting but fails.
|
|
*/
|
|
static int
|
|
dsa_switch_connect_tag_proto(struct dsa_switch *ds,
|
|
struct dsa_notifier_tag_proto_info *info)
|
|
{
|
|
const struct dsa_device_ops *tag_ops = info->tag_ops;
|
|
int err;
|
|
|
|
/* Notify the new tagger about the connection to this switch */
|
|
if (tag_ops->connect) {
|
|
err = tag_ops->connect(ds);
|
|
if (err)
|
|
return err;
|
|
}
|
|
|
|
if (!ds->ops->connect_tag_protocol)
|
|
return -EOPNOTSUPP;
|
|
|
|
/* Notify the switch about the connection to the new tagger */
|
|
err = ds->ops->connect_tag_protocol(ds, tag_ops->proto);
|
|
if (err) {
|
|
/* Revert the new tagger's connection to this tree */
|
|
if (tag_ops->disconnect)
|
|
tag_ops->disconnect(ds);
|
|
return err;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
dsa_switch_disconnect_tag_proto(struct dsa_switch *ds,
|
|
struct dsa_notifier_tag_proto_info *info)
|
|
{
|
|
const struct dsa_device_ops *tag_ops = info->tag_ops;
|
|
|
|
/* Notify the tagger about the disconnection from this switch */
|
|
if (tag_ops->disconnect && ds->tagger_data)
|
|
tag_ops->disconnect(ds);
|
|
|
|
/* No need to notify the switch, since it shouldn't have any
|
|
* resources to tear down
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
dsa_switch_master_state_change(struct dsa_switch *ds,
|
|
struct dsa_notifier_master_state_info *info)
|
|
{
|
|
if (!ds->ops->master_state_change)
|
|
return 0;
|
|
|
|
ds->ops->master_state_change(ds, info->master, info->operational);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_event(struct notifier_block *nb,
|
|
unsigned long event, void *info)
|
|
{
|
|
struct dsa_switch *ds = container_of(nb, struct dsa_switch, nb);
|
|
int err;
|
|
|
|
switch (event) {
|
|
case DSA_NOTIFIER_AGEING_TIME:
|
|
err = dsa_switch_ageing_time(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_BRIDGE_JOIN:
|
|
err = dsa_switch_bridge_join(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_BRIDGE_LEAVE:
|
|
err = dsa_switch_bridge_leave(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_FDB_ADD:
|
|
err = dsa_switch_fdb_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_FDB_DEL:
|
|
err = dsa_switch_fdb_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_FDB_ADD:
|
|
err = dsa_switch_host_fdb_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_FDB_DEL:
|
|
err = dsa_switch_host_fdb_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_LAG_FDB_ADD:
|
|
err = dsa_switch_lag_fdb_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_LAG_FDB_DEL:
|
|
err = dsa_switch_lag_fdb_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_LAG_CHANGE:
|
|
err = dsa_switch_lag_change(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_LAG_JOIN:
|
|
err = dsa_switch_lag_join(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_LAG_LEAVE:
|
|
err = dsa_switch_lag_leave(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_MDB_ADD:
|
|
err = dsa_switch_mdb_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_MDB_DEL:
|
|
err = dsa_switch_mdb_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_MDB_ADD:
|
|
err = dsa_switch_host_mdb_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_MDB_DEL:
|
|
err = dsa_switch_host_mdb_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_VLAN_ADD:
|
|
err = dsa_switch_vlan_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_VLAN_DEL:
|
|
err = dsa_switch_vlan_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_VLAN_ADD:
|
|
err = dsa_switch_host_vlan_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_HOST_VLAN_DEL:
|
|
err = dsa_switch_host_vlan_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_MTU:
|
|
err = dsa_switch_mtu(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_PROTO:
|
|
err = dsa_switch_change_tag_proto(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_PROTO_CONNECT:
|
|
err = dsa_switch_connect_tag_proto(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_PROTO_DISCONNECT:
|
|
err = dsa_switch_disconnect_tag_proto(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_8021Q_VLAN_ADD:
|
|
err = dsa_switch_tag_8021q_vlan_add(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_8021Q_VLAN_DEL:
|
|
err = dsa_switch_tag_8021q_vlan_del(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_MASTER_STATE_CHANGE:
|
|
err = dsa_switch_master_state_change(ds, info);
|
|
break;
|
|
default:
|
|
err = -EOPNOTSUPP;
|
|
break;
|
|
}
|
|
|
|
if (err)
|
|
dev_dbg(ds->dev, "breaking chain for DSA event %lu (%d)\n",
|
|
event, err);
|
|
|
|
return notifier_from_errno(err);
|
|
}
|
|
|
|
int dsa_switch_register_notifier(struct dsa_switch *ds)
|
|
{
|
|
ds->nb.notifier_call = dsa_switch_event;
|
|
|
|
return raw_notifier_chain_register(&ds->dst->nh, &ds->nb);
|
|
}
|
|
|
|
void dsa_switch_unregister_notifier(struct dsa_switch *ds)
|
|
{
|
|
int err;
|
|
|
|
err = raw_notifier_chain_unregister(&ds->dst->nh, &ds->nb);
|
|
if (err)
|
|
dev_err(ds->dev, "failed to unregister notifier (%d)\n", err);
|
|
}
|