net: dsa: let drivers state that they need VLAN filtering while standalone
authorVladimir Oltean <vladimir.oltean@nxp.com>
Mon, 23 Aug 2021 21:22:58 +0000 (00:22 +0300)
committerDavid S. Miller <davem@davemloft.net>
Tue, 24 Aug 2021 08:30:58 +0000 (09:30 +0100)
As explained in commit e358bef7c392 ("net: dsa: Give drivers the chance
to veto certain upper devices"), the hellcreek driver uses some tricks
to comply with the network stack expectations: it enforces port
separation in standalone mode using VLANs. For untagged traffic,
bridging between ports is prevented by using different PVIDs, and for
VLAN-tagged traffic, it never accepts 8021q uppers with the same VID on
two ports, so packets with one VLAN cannot leak from one port to another.

That is almost fine*, and has worked because hellcreek relied on an
implicit behavior of the DSA core that was changed by the previous
patch: the standalone ports declare the 'rx-vlan-filter' feature as 'on
[fixed]'. Since most of the DSA drivers are actually VLAN-unaware in
standalone mode, that feature was actually incorrectly reflecting the
hardware/driver state, so there was a desire to fix it. This leaves the
hellcreek driver in a situation where it has to explicitly request this
behavior from the DSA framework.

We configure the ports as follows:

- Standalone: 'rx-vlan-filter' is on. An 8021q upper on top of a
  standalone hellcreek port will go through dsa_slave_vlan_rx_add_vid
  and will add a VLAN to the hardware tables, giving the driver the
  opportunity to refuse it through .port_prechangeupper.

- Bridged with vlan_filtering=0: 'rx-vlan-filter' is off. An 8021q upper
  on top of a bridged hellcreek port will not go through
  dsa_slave_vlan_rx_add_vid, because there will not be any attempt to
  offload this VLAN. The driver already disables VLAN awareness, so that
  upper should receive the traffic it needs.

- Bridged with vlan_filtering=1: 'rx-vlan-filter' is on. An 8021q upper
  on top of a bridged hellcreek port will call dsa_slave_vlan_rx_add_vid,
  and can again be vetoed through .port_prechangeupper.

*It is not actually completely fine, because if I follow through
correctly, we can have the following situation:

ip link add br0 type bridge vlan_filtering 0
ip link set lan0 master br0 # lan0 now becomes VLAN-unaware
ip link set lan0 nomaster # lan0 fails to become VLAN-aware again, therefore breaking isolation

This patch fixes that corner case by extending the DSA core logic, based
on this requested attribute, to change the VLAN awareness state of the
switch (port) when it leaves the bridge.

Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
Reviewed-by: Florian Fainelli <f.fainelli@gmail.com>
Acked-by: Kurt Kanzenbach <kurt@linutronix.de>
Signed-off-by: David S. Miller <davem@davemloft.net>
drivers/net/dsa/hirschmann/hellcreek.c
include/net/dsa.h
net/dsa/slave.c
net/dsa/switch.c

index 5c54ae1..3faff95 100644 (file)
@@ -1345,6 +1345,7 @@ static int hellcreek_setup(struct dsa_switch *ds)
         * filtering setups are not supported.
         */
        ds->vlan_filtering_is_global = true;
+       ds->needs_standalone_vlan_filtering = true;
 
        /* Intercept _all_ PTP multicast traffic */
        ret = hellcreek_setup_fdb(hellcreek);
index c7ea0f6..f9a1714 100644 (file)
@@ -363,6 +363,9 @@ struct dsa_switch {
         */
        bool                    vlan_filtering_is_global;
 
+       /* Keep VLAN filtering enabled on ports not offloading any upper. */
+       bool                    needs_standalone_vlan_filtering;
+
        /* Pass .port_vlan_add and .port_vlan_del to drivers even for bridges
         * that have vlan_filtering=0. All drivers should ideally set this (and
         * then the option would get removed), but it is unknown whether this
index f71d31d..662ff53 100644 (file)
@@ -1435,11 +1435,12 @@ static int dsa_slave_clear_vlan(struct net_device *vdev, int vid, void *arg)
  * To summarize, a DSA switch port offloads:
  *
  * - If standalone (this includes software bridge, software LAG):
- *     - if ds->vlan_filtering_is_global = true AND there are bridges spanning
- *       this switch chip which have vlan_filtering=1:
+ *     - if ds->needs_standalone_vlan_filtering = true, OR if
+ *       (ds->vlan_filtering_is_global = true AND there are bridges spanning
+ *       this switch chip which have vlan_filtering=1)
  *         - the 8021q upper VLANs
- *     - else (VLAN filtering is not global, or it is, but no port is under a
- *       VLAN-aware bridge):
+ *     - else (standalone VLAN filtering is not needed, VLAN filtering is not
+ *       global, or it is, but no port is under a VLAN-aware bridge):
  *         - no VLAN (any 8021q upper is a software VLAN)
  *
  * - If under a vlan_filtering=0 bridge which it offload:
@@ -1871,6 +1872,7 @@ void dsa_slave_setup_tagger(struct net_device *slave)
        struct dsa_slave_priv *p = netdev_priv(slave);
        const struct dsa_port *cpu_dp = dp->cpu_dp;
        struct net_device *master = cpu_dp->master;
+       const struct dsa_switch *ds = dp->ds;
 
        slave->needed_headroom = cpu_dp->tag_ops->needed_headroom;
        slave->needed_tailroom = cpu_dp->tag_ops->needed_tailroom;
@@ -1888,6 +1890,8 @@ void dsa_slave_setup_tagger(struct net_device *slave)
        slave->features |= NETIF_F_LLTX;
        if (slave->needed_tailroom)
                slave->features &= ~(NETIF_F_SG | NETIF_F_FRAGLIST);
+       if (ds->needs_standalone_vlan_filtering)
+               slave->features |= NETIF_F_HW_VLAN_CTAG_FILTER;
 }
 
 static struct lock_class_key dsa_slave_netdev_xmit_lock_key;
index dd042fd..1c797ec 100644 (file)
@@ -116,9 +116,10 @@ static int dsa_switch_bridge_join(struct dsa_switch *ds,
 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;
        struct netlink_ext_ack extack = {0};
+       bool change_vlan_filtering = false;
+       bool vlan_filtering;
        int err, port;
 
        if (dst->index == info->tree_index && ds->index == info->sw_index &&
@@ -131,6 +132,15 @@ static int dsa_switch_bridge_leave(struct dsa_switch *ds,
                                                info->sw_index, info->port,
                                                info->br);
 
+       if (ds->needs_standalone_vlan_filtering && !br_vlan_enabled(info->br)) {
+               change_vlan_filtering = true;
+               vlan_filtering = true;
+       } else if (!ds->needs_standalone_vlan_filtering &&
+                  br_vlan_enabled(info->br)) {
+               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
@@ -139,21 +149,22 @@ static int dsa_switch_bridge_leave(struct dsa_switch *ds,
         * vlan_filtering callback is only when the last port leaves the last
         * VLAN-aware bridge.
         */
-       if (unset_vlan_filtering && ds->vlan_filtering_is_global) {
+       if (change_vlan_filtering && ds->vlan_filtering_is_global) {
                for (port = 0; port < ds->num_ports; port++) {
                        struct net_device *bridge_dev;
 
                        bridge_dev = dsa_to_port(ds, port)->bridge_dev;
 
                        if (bridge_dev && br_vlan_enabled(bridge_dev)) {
-                               unset_vlan_filtering = false;
+                               change_vlan_filtering = false;
                                break;
                        }
                }
        }
-       if (unset_vlan_filtering) {
+
+       if (change_vlan_filtering) {
                err = dsa_port_vlan_filtering(dsa_to_port(ds, info->port),
-                                             false, &extack);
+                                             vlan_filtering, &extack);
                if (extack._msg)
                        dev_err(ds->dev, "port %d: %s\n", info->port,
                                extack._msg);