root/drivers/net/ethernet/microchip/lan966x/lan966x_mdb.c
// SPDX-License-Identifier: GPL-2.0+

#include <net/switchdev.h>

#include "lan966x_main.h"

struct lan966x_pgid_entry {
        struct list_head list;
        int index;
        refcount_t refcount;
        u16 ports;
};

struct lan966x_mdb_entry {
        struct list_head list;
        unsigned char mac[ETH_ALEN];
        u16 vid;
        u16 ports;
        struct lan966x_pgid_entry *pgid;
        u8 cpu_copy;
};

void lan966x_mdb_init(struct lan966x *lan966x)
{
        INIT_LIST_HEAD(&lan966x->mdb_entries);
        INIT_LIST_HEAD(&lan966x->pgid_entries);
}

static void lan966x_mdb_purge_mdb_entries(struct lan966x *lan966x)
{
        struct lan966x_mdb_entry *mdb_entry, *tmp;

        list_for_each_entry_safe(mdb_entry, tmp, &lan966x->mdb_entries, list) {
                list_del(&mdb_entry->list);
                kfree(mdb_entry);
        }
}

static void lan966x_mdb_purge_pgid_entries(struct lan966x *lan966x)
{
        struct lan966x_pgid_entry *pgid_entry, *tmp;

        list_for_each_entry_safe(pgid_entry, tmp, &lan966x->pgid_entries, list) {
                list_del(&pgid_entry->list);
                kfree(pgid_entry);
        }
}

void lan966x_mdb_deinit(struct lan966x *lan966x)
{
        lan966x_mdb_purge_mdb_entries(lan966x);
        lan966x_mdb_purge_pgid_entries(lan966x);
}

static struct lan966x_mdb_entry *
lan966x_mdb_entry_get(struct lan966x *lan966x,
                      const unsigned char *mac,
                      u16 vid)
{
        struct lan966x_mdb_entry *mdb_entry;

        list_for_each_entry(mdb_entry, &lan966x->mdb_entries, list) {
                if (ether_addr_equal(mdb_entry->mac, mac) &&
                    mdb_entry->vid == vid)
                        return mdb_entry;
        }

        return NULL;
}

static struct lan966x_mdb_entry *
lan966x_mdb_entry_add(struct lan966x *lan966x,
                      const struct switchdev_obj_port_mdb *mdb)
{
        struct lan966x_mdb_entry *mdb_entry;

        mdb_entry = kzalloc_obj(*mdb_entry);
        if (!mdb_entry)
                return ERR_PTR(-ENOMEM);

        ether_addr_copy(mdb_entry->mac, mdb->addr);
        mdb_entry->vid = mdb->vid;

        list_add_tail(&mdb_entry->list, &lan966x->mdb_entries);

        return mdb_entry;
}

static void lan966x_mdb_encode_mac(unsigned char *mac,
                                   struct lan966x_mdb_entry *mdb_entry,
                                   enum macaccess_entry_type type)
{
        ether_addr_copy(mac, mdb_entry->mac);

        if (type == ENTRYTYPE_MACV4) {
                mac[0] = 0;
                mac[1] = mdb_entry->ports >> 8;
                mac[2] = mdb_entry->ports & 0xff;
        } else if (type == ENTRYTYPE_MACV6) {
                mac[0] = mdb_entry->ports >> 8;
                mac[1] = mdb_entry->ports & 0xff;
        }
}

static int lan966x_mdb_ip_add(struct lan966x_port *port,
                              const struct switchdev_obj_port_mdb *mdb,
                              enum macaccess_entry_type type)
{
        bool cpu_port = netif_is_bridge_master(mdb->obj.orig_dev);
        struct lan966x *lan966x = port->lan966x;
        struct lan966x_mdb_entry *mdb_entry;
        unsigned char mac[ETH_ALEN];
        bool cpu_copy = false;

        mdb_entry = lan966x_mdb_entry_get(lan966x, mdb->addr, mdb->vid);
        if (!mdb_entry) {
                mdb_entry = lan966x_mdb_entry_add(lan966x, mdb);
                if (IS_ERR(mdb_entry))
                        return PTR_ERR(mdb_entry);
        } else {
                lan966x_mdb_encode_mac(mac, mdb_entry, type);
                lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        }

        if (cpu_port)
                mdb_entry->cpu_copy++;
        else
                mdb_entry->ports |= BIT(port->chip_port);

        /* Copy the frame to CPU only if the CPU is in the VLAN */
        if (lan966x_vlan_cpu_member_cpu_vlan_mask(lan966x, mdb_entry->vid) &&
            mdb_entry->cpu_copy)
                cpu_copy = true;

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        return lan966x_mac_ip_learn(lan966x, cpu_copy,
                                    mac, mdb_entry->vid, type);
}

static int lan966x_mdb_ip_del(struct lan966x_port *port,
                              const struct switchdev_obj_port_mdb *mdb,
                              enum macaccess_entry_type type)
{
        bool cpu_port = netif_is_bridge_master(mdb->obj.orig_dev);
        struct lan966x *lan966x = port->lan966x;
        struct lan966x_mdb_entry *mdb_entry;
        unsigned char mac[ETH_ALEN];
        u16 ports;

        mdb_entry = lan966x_mdb_entry_get(lan966x, mdb->addr, mdb->vid);
        if (!mdb_entry)
                return -ENOENT;

        ports = mdb_entry->ports;
        if (cpu_port) {
                /* If there are still other references to the CPU port then
                 * there is no point to delete and add again the same entry
                 */
                mdb_entry->cpu_copy--;
                if (mdb_entry->cpu_copy)
                        return 0;
        } else {
                ports &= ~BIT(port->chip_port);
        }

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);

        mdb_entry->ports = ports;

        if (!mdb_entry->ports && !mdb_entry->cpu_copy) {
                list_del(&mdb_entry->list);
                kfree(mdb_entry);
                return 0;
        }

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        return lan966x_mac_ip_learn(lan966x, mdb_entry->cpu_copy,
                                    mac, mdb_entry->vid, type);
}

static struct lan966x_pgid_entry *
lan966x_pgid_entry_add(struct lan966x *lan966x, int index, u16 ports)
{
        struct lan966x_pgid_entry *pgid_entry;

        pgid_entry = kzalloc_obj(*pgid_entry);
        if (!pgid_entry)
                return ERR_PTR(-ENOMEM);

        pgid_entry->ports = ports;
        pgid_entry->index = index;
        refcount_set(&pgid_entry->refcount, 1);

        list_add_tail(&pgid_entry->list, &lan966x->pgid_entries);

        return pgid_entry;
}

static struct lan966x_pgid_entry *
lan966x_pgid_entry_get(struct lan966x *lan966x,
                       struct lan966x_mdb_entry *mdb_entry)
{
        struct lan966x_pgid_entry *pgid_entry;
        int index;

        /* Try to find an existing pgid that uses the same ports as the
         * mdb_entry
         */
        list_for_each_entry(pgid_entry, &lan966x->pgid_entries, list) {
                if (pgid_entry->ports == mdb_entry->ports) {
                        refcount_inc(&pgid_entry->refcount);
                        return pgid_entry;
                }
        }

        /* Try to find an empty pgid entry and allocate one in case it finds it,
         * otherwise it means that there are no more resources
         */
        for (index = PGID_GP_START; index < PGID_GP_END; index++) {
                bool used = false;

                list_for_each_entry(pgid_entry, &lan966x->pgid_entries, list) {
                        if (pgid_entry->index == index) {
                                used = true;
                                break;
                        }
                }

                if (!used)
                        return lan966x_pgid_entry_add(lan966x, index,
                                                      mdb_entry->ports);
        }

        return ERR_PTR(-ENOSPC);
}

static void lan966x_pgid_entry_del(struct lan966x *lan966x,
                                   struct lan966x_pgid_entry *pgid_entry)
{
        if (!refcount_dec_and_test(&pgid_entry->refcount))
                return;

        list_del(&pgid_entry->list);
        kfree(pgid_entry);
}

static int lan966x_mdb_l2_add(struct lan966x_port *port,
                              const struct switchdev_obj_port_mdb *mdb,
                              enum macaccess_entry_type type)
{
        bool cpu_port = netif_is_bridge_master(mdb->obj.orig_dev);
        struct lan966x *lan966x = port->lan966x;
        struct lan966x_pgid_entry *pgid_entry;
        struct lan966x_mdb_entry *mdb_entry;
        unsigned char mac[ETH_ALEN];

        mdb_entry = lan966x_mdb_entry_get(lan966x, mdb->addr, mdb->vid);
        if (!mdb_entry) {
                mdb_entry = lan966x_mdb_entry_add(lan966x, mdb);
                if (IS_ERR(mdb_entry))
                        return PTR_ERR(mdb_entry);
        } else {
                lan966x_pgid_entry_del(lan966x, mdb_entry->pgid);
                lan966x_mdb_encode_mac(mac, mdb_entry, type);
                lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        }

        if (cpu_port) {
                mdb_entry->ports |= BIT(CPU_PORT);
                mdb_entry->cpu_copy++;
        } else {
                mdb_entry->ports |= BIT(port->chip_port);
        }

        pgid_entry = lan966x_pgid_entry_get(lan966x, mdb_entry);
        if (IS_ERR(pgid_entry)) {
                list_del(&mdb_entry->list);
                kfree(mdb_entry);
                return PTR_ERR(pgid_entry);
        }
        mdb_entry->pgid = pgid_entry;

        /* Copy the frame to CPU only if the CPU is in the VLAN */
        if (!lan966x_vlan_cpu_member_cpu_vlan_mask(lan966x, mdb_entry->vid) &&
            mdb_entry->cpu_copy)
                mdb_entry->ports &= BIT(CPU_PORT);

        lan_rmw(ANA_PGID_PGID_SET(mdb_entry->ports),
                ANA_PGID_PGID,
                lan966x, ANA_PGID(pgid_entry->index));

        return lan966x_mac_learn(lan966x, pgid_entry->index, mdb_entry->mac,
                                 mdb_entry->vid, type);
}

static int lan966x_mdb_l2_del(struct lan966x_port *port,
                              const struct switchdev_obj_port_mdb *mdb,
                              enum macaccess_entry_type type)
{
        bool cpu_port = netif_is_bridge_master(mdb->obj.orig_dev);
        struct lan966x *lan966x = port->lan966x;
        struct lan966x_pgid_entry *pgid_entry;
        struct lan966x_mdb_entry *mdb_entry;
        unsigned char mac[ETH_ALEN];
        u16 ports;

        mdb_entry = lan966x_mdb_entry_get(lan966x, mdb->addr, mdb->vid);
        if (!mdb_entry)
                return -ENOENT;

        ports = mdb_entry->ports;
        if (cpu_port) {
                /* If there are still other references to the CPU port then
                 * there is no point to delete and add again the same entry
                 */
                mdb_entry->cpu_copy--;
                if (mdb_entry->cpu_copy)
                        return 0;

                ports &= ~BIT(CPU_PORT);
        } else {
                ports &= ~BIT(port->chip_port);
        }

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        lan966x_pgid_entry_del(lan966x, mdb_entry->pgid);

        mdb_entry->ports = ports;

        if (!mdb_entry->ports) {
                list_del(&mdb_entry->list);
                kfree(mdb_entry);
                return 0;
        }

        pgid_entry = lan966x_pgid_entry_get(lan966x, mdb_entry);
        if (IS_ERR(pgid_entry)) {
                list_del(&mdb_entry->list);
                kfree(mdb_entry);
                return PTR_ERR(pgid_entry);
        }
        mdb_entry->pgid = pgid_entry;

        lan_rmw(ANA_PGID_PGID_SET(mdb_entry->ports),
                ANA_PGID_PGID,
                lan966x, ANA_PGID(pgid_entry->index));

        return lan966x_mac_learn(lan966x, pgid_entry->index, mdb_entry->mac,
                                 mdb_entry->vid, type);
}

static enum macaccess_entry_type
lan966x_mdb_classify(const unsigned char *mac)
{
        if (mac[0] == 0x01 && mac[1] == 0x00 && mac[2] == 0x5e)
                return ENTRYTYPE_MACV4;
        if (mac[0] == 0x33 && mac[1] == 0x33)
                return ENTRYTYPE_MACV6;
        return ENTRYTYPE_LOCKED;
}

int lan966x_handle_port_mdb_add(struct lan966x_port *port,
                                const struct switchdev_obj *obj)
{
        const struct switchdev_obj_port_mdb *mdb = SWITCHDEV_OBJ_PORT_MDB(obj);
        enum macaccess_entry_type type;

        /* Split the way the entries are added for ipv4/ipv6 and for l2. The
         * reason is that for ipv4/ipv6 it doesn't require to use any pgid
         * entry, while for l2 is required to use pgid entries
         */
        type = lan966x_mdb_classify(mdb->addr);
        if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6)
                return lan966x_mdb_ip_add(port, mdb, type);

        return lan966x_mdb_l2_add(port, mdb, type);
}

int lan966x_handle_port_mdb_del(struct lan966x_port *port,
                                const struct switchdev_obj *obj)
{
        const struct switchdev_obj_port_mdb *mdb = SWITCHDEV_OBJ_PORT_MDB(obj);
        enum macaccess_entry_type type;

        /* Split the way the entries are removed for ipv4/ipv6 and for l2. The
         * reason is that for ipv4/ipv6 it doesn't require to use any pgid
         * entry, while for l2 is required to use pgid entries
         */
        type = lan966x_mdb_classify(mdb->addr);
        if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6)
                return lan966x_mdb_ip_del(port, mdb, type);

        return lan966x_mdb_l2_del(port, mdb, type);
}

static void lan966x_mdb_ip_cpu_copy(struct lan966x *lan966x,
                                    struct lan966x_mdb_entry *mdb_entry,
                                    enum macaccess_entry_type type)
{
        unsigned char mac[ETH_ALEN];

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        lan966x_mac_ip_learn(lan966x, true, mac, mdb_entry->vid, type);
}

static void lan966x_mdb_l2_cpu_copy(struct lan966x *lan966x,
                                    struct lan966x_mdb_entry *mdb_entry,
                                    enum macaccess_entry_type type)
{
        struct lan966x_pgid_entry *pgid_entry;
        unsigned char mac[ETH_ALEN];

        lan966x_pgid_entry_del(lan966x, mdb_entry->pgid);
        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);

        mdb_entry->ports |= BIT(CPU_PORT);

        pgid_entry = lan966x_pgid_entry_get(lan966x, mdb_entry);
        if (IS_ERR(pgid_entry))
                return;

        mdb_entry->pgid = pgid_entry;

        lan_rmw(ANA_PGID_PGID_SET(mdb_entry->ports),
                ANA_PGID_PGID,
                lan966x, ANA_PGID(pgid_entry->index));

        lan966x_mac_learn(lan966x, pgid_entry->index, mdb_entry->mac,
                          mdb_entry->vid, type);
}

void lan966x_mdb_write_entries(struct lan966x *lan966x, u16 vid)
{
        struct lan966x_mdb_entry *mdb_entry;
        enum macaccess_entry_type type;

        list_for_each_entry(mdb_entry, &lan966x->mdb_entries, list) {
                if (mdb_entry->vid != vid || !mdb_entry->cpu_copy)
                        continue;

                type = lan966x_mdb_classify(mdb_entry->mac);
                if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6)
                        lan966x_mdb_ip_cpu_copy(lan966x, mdb_entry, type);
                else
                        lan966x_mdb_l2_cpu_copy(lan966x, mdb_entry, type);
        }
}

static void lan966x_mdb_ip_cpu_remove(struct lan966x *lan966x,
                                      struct lan966x_mdb_entry *mdb_entry,
                                      enum macaccess_entry_type type)
{
        unsigned char mac[ETH_ALEN];

        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        lan966x_mac_ip_learn(lan966x, false, mac, mdb_entry->vid, type);
}

static void lan966x_mdb_l2_cpu_remove(struct lan966x *lan966x,
                                      struct lan966x_mdb_entry *mdb_entry,
                                      enum macaccess_entry_type type)
{
        struct lan966x_pgid_entry *pgid_entry;
        unsigned char mac[ETH_ALEN];

        lan966x_pgid_entry_del(lan966x, mdb_entry->pgid);
        lan966x_mdb_encode_mac(mac, mdb_entry, type);
        lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);

        mdb_entry->ports &= ~BIT(CPU_PORT);

        pgid_entry = lan966x_pgid_entry_get(lan966x, mdb_entry);
        if (IS_ERR(pgid_entry))
                return;

        mdb_entry->pgid = pgid_entry;

        lan_rmw(ANA_PGID_PGID_SET(mdb_entry->ports),
                ANA_PGID_PGID,
                lan966x, ANA_PGID(pgid_entry->index));

        lan966x_mac_learn(lan966x, pgid_entry->index, mdb_entry->mac,
                          mdb_entry->vid, type);
}

void lan966x_mdb_erase_entries(struct lan966x *lan966x, u16 vid)
{
        struct lan966x_mdb_entry *mdb_entry;
        enum macaccess_entry_type type;

        list_for_each_entry(mdb_entry, &lan966x->mdb_entries, list) {
                if (mdb_entry->vid != vid || !mdb_entry->cpu_copy)
                        continue;

                type = lan966x_mdb_classify(mdb_entry->mac);
                if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6)
                        lan966x_mdb_ip_cpu_remove(lan966x, mdb_entry, type);
                else
                        lan966x_mdb_l2_cpu_remove(lan966x, mdb_entry, type);
        }
}

void lan966x_mdb_clear_entries(struct lan966x *lan966x)
{
        struct lan966x_mdb_entry *mdb_entry;
        enum macaccess_entry_type type;
        unsigned char mac[ETH_ALEN];

        list_for_each_entry(mdb_entry, &lan966x->mdb_entries, list) {
                type = lan966x_mdb_classify(mdb_entry->mac);

                lan966x_mdb_encode_mac(mac, mdb_entry, type);
                /* Remove just the MAC entry, still keep the PGID in case of L2
                 * entries because this can be restored at later point
                 */
                lan966x_mac_forget(lan966x, mac, mdb_entry->vid, type);
        }
}

void lan966x_mdb_restore_entries(struct lan966x *lan966x)
{
        struct lan966x_mdb_entry *mdb_entry;
        enum macaccess_entry_type type;
        unsigned char mac[ETH_ALEN];
        bool cpu_copy = false;

        list_for_each_entry(mdb_entry, &lan966x->mdb_entries, list) {
                type = lan966x_mdb_classify(mdb_entry->mac);

                lan966x_mdb_encode_mac(mac, mdb_entry, type);
                if (type == ENTRYTYPE_MACV4 || type == ENTRYTYPE_MACV6) {
                        /* Copy the frame to CPU only if the CPU is in the VLAN */
                        if (lan966x_vlan_cpu_member_cpu_vlan_mask(lan966x,
                                                                  mdb_entry->vid) &&
                            mdb_entry->cpu_copy)
                                cpu_copy = true;

                        lan966x_mac_ip_learn(lan966x, cpu_copy, mac,
                                             mdb_entry->vid, type);
                } else {
                        lan966x_mac_learn(lan966x, mdb_entry->pgid->index,
                                          mdb_entry->mac,
                                          mdb_entry->vid, type);
                }
        }
}