mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2025-01-16 18:26:42 +00:00
53da0ebaad
Currently DSA exposes the following sysfs: $ cat /sys/class/net/eno2/dsa/tagging ocelot which is a read-only device attribute, introduced in the kernel as commit 98cdb4807123 ("net: dsa: Expose tagging protocol to user-space"), and used by libpcap since its commit 993db3800d7d ("Add support for DSA link-layer types"). It would be nice if we could extend this device attribute by making it writable: $ echo ocelot-8021q > /sys/class/net/eno2/dsa/tagging This is useful with DSA switches that can make use of more than one tagging protocol. It may be useful in dsa_loop in the future too, to perform offline testing of various taggers, or for changing between dsa and edsa on Marvell switches, if that is desirable. In terms of implementation, drivers can support this feature by implementing .change_tag_protocol, which should always leave the switch in a consistent state: either with the new protocol if things went well, or with the old one if something failed. Teardown of the old protocol, if necessary, must be handled by the driver. Some things remain as before: - The .get_tag_protocol is currently only called at probe time, to load the initial tagging protocol driver. Nonetheless, new drivers should report the tagging protocol in current use now. - The driver should manage by itself the initial setup of tagging protocol, no later than the .setup() method, as well as destroying resources used by the last tagger in use, no earlier than the .teardown() method. For multi-switch DSA trees, error handling is a bit more complicated, since e.g. the 5th out of 7 switches may fail to change the tag protocol. When that happens, a revert to the original tag protocol is attempted, but that may fail too, leaving the tree in an inconsistent state despite each individual switch implementing .change_tag_protocol transactionally. Since the intersection between drivers that implement .change_tag_protocol and drivers that support D in DSA is currently the empty set, the possibility for this error to happen is ignored for now. Testing: $ insmod mscc_felix.ko [ 79.549784] mscc_felix 0000:00:00.5: Adding to iommu group 14 [ 79.565712] mscc_felix 0000:00:00.5: Failed to register DSA switch: -517 $ insmod tag_ocelot.ko $ rmmod mscc_felix.ko $ insmod mscc_felix.ko [ 97.261724] libphy: VSC9959 internal MDIO bus: probed [ 97.267363] mscc_felix 0000:00:00.5: Found PCS at internal MDIO address 0 [ 97.274998] mscc_felix 0000:00:00.5: Found PCS at internal MDIO address 1 [ 97.282561] mscc_felix 0000:00:00.5: Found PCS at internal MDIO address 2 [ 97.289700] mscc_felix 0000:00:00.5: Found PCS at internal MDIO address 3 [ 97.599163] mscc_felix 0000:00:00.5 swp0 (uninitialized): PHY [0000:00:00.3:10] driver [Microsemi GE VSC8514 SyncE] (irq=POLL) [ 97.862034] mscc_felix 0000:00:00.5 swp1 (uninitialized): PHY [0000:00:00.3:11] driver [Microsemi GE VSC8514 SyncE] (irq=POLL) [ 97.950731] mscc_felix 0000:00:00.5 swp0: configuring for inband/qsgmii link mode [ 97.964278] 8021q: adding VLAN 0 to HW filter on device swp0 [ 98.146161] mscc_felix 0000:00:00.5 swp2 (uninitialized): PHY [0000:00:00.3:12] driver [Microsemi GE VSC8514 SyncE] (irq=POLL) [ 98.238649] mscc_felix 0000:00:00.5 swp1: configuring for inband/qsgmii link mode [ 98.251845] 8021q: adding VLAN 0 to HW filter on device swp1 [ 98.433916] mscc_felix 0000:00:00.5 swp3 (uninitialized): PHY [0000:00:00.3:13] driver [Microsemi GE VSC8514 SyncE] (irq=POLL) [ 98.485542] mscc_felix 0000:00:00.5: configuring for fixed/internal link mode [ 98.503584] mscc_felix 0000:00:00.5: Link is Up - 2.5Gbps/Full - flow control rx/tx [ 98.527948] device eno2 entered promiscuous mode [ 98.544755] DSA: tree 0 setup $ ping 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes 64 bytes from 10.0.0.1: seq=0 ttl=64 time=2.337 ms 64 bytes from 10.0.0.1: seq=1 ttl=64 time=0.754 ms ^C - 10.0.0.1 ping statistics - 2 packets transmitted, 2 packets received, 0% packet loss round-trip min/avg/max = 0.754/1.545/2.337 ms $ cat /sys/class/net/eno2/dsa/tagging ocelot $ cat ./test_ocelot_8021q.sh #!/bin/bash ip link set swp0 down ip link set swp1 down ip link set swp2 down ip link set swp3 down ip link set swp5 down ip link set eno2 down echo ocelot-8021q > /sys/class/net/eno2/dsa/tagging ip link set eno2 up ip link set swp0 up ip link set swp1 up ip link set swp2 up ip link set swp3 up ip link set swp5 up $ ./test_ocelot_8021q.sh ./test_ocelot_8021q.sh: line 9: echo: write error: Protocol not available $ rmmod tag_ocelot.ko rmmod: can't unload module 'tag_ocelot': Resource temporarily unavailable $ insmod tag_ocelot_8021q.ko $ ./test_ocelot_8021q.sh $ cat /sys/class/net/eno2/dsa/tagging ocelot-8021q $ rmmod tag_ocelot.ko $ rmmod tag_ocelot_8021q.ko rmmod: can't unload module 'tag_ocelot_8021q': Resource temporarily unavailable $ ping 10.0.0.1 PING 10.0.0.1 (10.0.0.1): 56 data bytes 64 bytes from 10.0.0.1: seq=0 ttl=64 time=0.953 ms 64 bytes from 10.0.0.1: seq=1 ttl=64 time=0.787 ms 64 bytes from 10.0.0.1: seq=2 ttl=64 time=0.771 ms $ rmmod mscc_felix.ko [ 645.544426] mscc_felix 0000:00:00.5: Link is Down [ 645.838608] DSA: tree 0 torn down $ rmmod tag_ocelot_8021q.ko Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com> Signed-off-by: Jakub Kicinski <kuba@kernel.org>
428 lines
10 KiB
C
428 lines
10 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)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < ds->num_ports; ++i) {
|
|
struct dsa_port *dp = dsa_to_port(ds, i);
|
|
|
|
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_switch_mtu_match(struct dsa_switch *ds, int port,
|
|
struct dsa_notifier_mtu_info *info)
|
|
{
|
|
if (ds->index == info->sw_index)
|
|
return (port == info->port) || dsa_is_dsa_port(ds, port);
|
|
|
|
if (!info->propagate_upstream)
|
|
return false;
|
|
|
|
if (dsa_is_dsa_port(ds, port) || dsa_is_cpu_port(ds, port))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static int dsa_switch_mtu(struct dsa_switch *ds,
|
|
struct dsa_notifier_mtu_info *info)
|
|
{
|
|
int port, ret;
|
|
|
|
if (!ds->ops->port_change_mtu)
|
|
return -EOPNOTSUPP;
|
|
|
|
for (port = 0; port < ds->num_ports; port++) {
|
|
if (dsa_switch_mtu_match(ds, port, info)) {
|
|
ret = ds->ops->port_change_mtu(ds, port, 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;
|
|
|
|
if (dst->index == info->tree_index && ds->index == info->sw_index &&
|
|
ds->ops->port_bridge_join)
|
|
return ds->ops->port_bridge_join(ds, info->port, info->br);
|
|
|
|
if ((dst->index != info->tree_index || ds->index != info->sw_index) &&
|
|
ds->ops->crosschip_bridge_join)
|
|
return ds->ops->crosschip_bridge_join(ds, info->tree_index,
|
|
info->sw_index,
|
|
info->port, info->br);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_bridge_leave(struct dsa_switch *ds,
|
|
struct dsa_notifier_bridge_info *info)
|
|
{
|
|
bool unset_vlan_filtering = br_vlan_enabled(info->br);
|
|
struct dsa_switch_tree *dst = ds->dst;
|
|
int err, i;
|
|
|
|
if (dst->index == info->tree_index && ds->index == info->sw_index &&
|
|
ds->ops->port_bridge_join)
|
|
ds->ops->port_bridge_leave(ds, info->port, info->br);
|
|
|
|
if ((dst->index != info->tree_index || ds->index != info->sw_index) &&
|
|
ds->ops->crosschip_bridge_join)
|
|
ds->ops->crosschip_bridge_leave(ds, info->tree_index,
|
|
info->sw_index, info->port,
|
|
info->br);
|
|
|
|
/* 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 left this bridge.
|
|
*/
|
|
if (unset_vlan_filtering && ds->vlan_filtering_is_global) {
|
|
for (i = 0; i < ds->num_ports; i++) {
|
|
if (i == info->port)
|
|
continue;
|
|
if (dsa_to_port(ds, i)->bridge_dev == info->br) {
|
|
unset_vlan_filtering = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (unset_vlan_filtering) {
|
|
err = dsa_port_vlan_filtering(dsa_to_port(ds, info->port),
|
|
false);
|
|
if (err && err != EOPNOTSUPP)
|
|
return err;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
|
|
if (!ds->ops->port_fdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
return ds->ops->port_fdb_add(ds, port, info->addr, info->vid);
|
|
}
|
|
|
|
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);
|
|
|
|
if (!ds->ops->port_fdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
return ds->ops->port_fdb_del(ds, port, info->addr, info->vid);
|
|
}
|
|
|
|
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 0;
|
|
}
|
|
|
|
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 0;
|
|
}
|
|
|
|
static bool dsa_switch_mdb_match(struct dsa_switch *ds, int port,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
if (ds->index == info->sw_index && port == info->port)
|
|
return true;
|
|
|
|
if (dsa_is_dsa_port(ds, port))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static int dsa_switch_mdb_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
int err = 0;
|
|
int port;
|
|
|
|
if (!ds->ops->port_mdb_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
for (port = 0; port < ds->num_ports; port++) {
|
|
if (dsa_switch_mdb_match(ds, port, info)) {
|
|
err = ds->ops->port_mdb_add(ds, port, info->mdb);
|
|
if (err)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static int dsa_switch_mdb_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_mdb_info *info)
|
|
{
|
|
if (!ds->ops->port_mdb_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
if (ds->index == info->sw_index)
|
|
return ds->ops->port_mdb_del(ds, info->port, info->mdb);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static bool dsa_switch_vlan_match(struct dsa_switch *ds, int port,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
if (ds->index == info->sw_index && port == info->port)
|
|
return true;
|
|
|
|
if (dsa_is_dsa_port(ds, port))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static int dsa_switch_vlan_add(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
int port, err;
|
|
|
|
if (!ds->ops->port_vlan_add)
|
|
return -EOPNOTSUPP;
|
|
|
|
for (port = 0; port < ds->num_ports; port++) {
|
|
if (dsa_switch_vlan_match(ds, port, info)) {
|
|
err = ds->ops->port_vlan_add(ds, port, info->vlan);
|
|
if (err)
|
|
return err;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int dsa_switch_vlan_del(struct dsa_switch *ds,
|
|
struct dsa_notifier_vlan_info *info)
|
|
{
|
|
if (!ds->ops->port_vlan_del)
|
|
return -EOPNOTSUPP;
|
|
|
|
if (ds->index == info->sw_index)
|
|
return ds->ops->port_vlan_del(ds, info->port, info->vlan);
|
|
|
|
/* Do not deprogram the DSA links as they may be used as conduit
|
|
* for other VLAN members in the fabric.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
static bool dsa_switch_tag_proto_match(struct dsa_switch *ds, int port,
|
|
struct dsa_notifier_tag_proto_info *info)
|
|
{
|
|
if (dsa_is_cpu_port(ds, port) || dsa_is_dsa_port(ds, port))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
int port, err;
|
|
|
|
if (!ds->ops->change_tag_protocol)
|
|
return -EOPNOTSUPP;
|
|
|
|
ASSERT_RTNL();
|
|
|
|
for (port = 0; port < ds->num_ports; port++) {
|
|
if (dsa_switch_tag_proto_match(ds, port, info)) {
|
|
err = ds->ops->change_tag_protocol(ds, port,
|
|
tag_ops->proto);
|
|
if (err)
|
|
return err;
|
|
|
|
if (dsa_is_cpu_port(ds, port))
|
|
dsa_port_set_tag_protocol(dsa_to_port(ds, port),
|
|
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.
|
|
*/
|
|
for (port = 0; port < ds->num_ports; port++) {
|
|
if (dsa_is_user_port(ds, port)) {
|
|
struct net_device *slave;
|
|
|
|
slave = dsa_to_port(ds, port)->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;
|
|
}
|
|
|
|
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_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_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_MTU:
|
|
err = dsa_switch_mtu(ds, info);
|
|
break;
|
|
case DSA_NOTIFIER_TAG_PROTO:
|
|
err = dsa_switch_change_tag_proto(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);
|
|
}
|