root/drivers/net/ethernet/pensando/ionic/ionic_rx_filter.c
// SPDX-License-Identifier: GPL-2.0
/* Copyright(c) 2017 - 2019 Pensando Systems, Inc */

#include <linux/netdevice.h>
#include <linux/dynamic_debug.h>
#include <linux/etherdevice.h>
#include <linux/list.h>

#include "ionic.h"
#include "ionic_lif.h"
#include "ionic_rx_filter.h"

void ionic_rx_filter_free(struct ionic_lif *lif, struct ionic_rx_filter *f)
{
        struct device *dev = lif->ionic->dev;

        hlist_del(&f->by_id);
        hlist_del(&f->by_hash);
        devm_kfree(dev, f);
}

void ionic_rx_filter_replay(struct ionic_lif *lif)
{
        struct ionic_rx_filter_add_cmd *ac;
        struct hlist_head new_id_list;
        struct ionic_admin_ctx ctx;
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        struct hlist_node *tmp;
        unsigned int key;
        unsigned int i;
        int err;

        INIT_HLIST_HEAD(&new_id_list);
        ac = &ctx.cmd.rx_filter_add;

        for (i = 0; i < IONIC_RX_FILTER_HLISTS; i++) {
                head = &lif->rx_filters.by_id[i];
                hlist_for_each_entry_safe(f, tmp, head, by_id) {
                        ctx.work = COMPLETION_INITIALIZER_ONSTACK(ctx.work);
                        memcpy(ac, &f->cmd, sizeof(f->cmd));
                        dev_dbg(&lif->netdev->dev, "replay filter command:\n");
                        dynamic_hex_dump("cmd ", DUMP_PREFIX_OFFSET, 16, 1,
                                         &ctx.cmd, sizeof(ctx.cmd), true);

                        err = ionic_adminq_post_wait(lif, &ctx);
                        if (err) {
                                switch (le16_to_cpu(ac->match)) {
                                case IONIC_RX_FILTER_MATCH_VLAN:
                                        netdev_info(lif->netdev, "Replay failed - %d: vlan %d\n",
                                                    err,
                                                    le16_to_cpu(ac->vlan.vlan));
                                        break;
                                case IONIC_RX_FILTER_MATCH_MAC:
                                        netdev_info(lif->netdev, "Replay failed - %d: mac %pM\n",
                                                    err, ac->mac.addr);
                                        break;
                                case IONIC_RX_FILTER_MATCH_MAC_VLAN:
                                        netdev_info(lif->netdev, "Replay failed - %d: vlan %d mac %pM\n",
                                                    err,
                                                    le16_to_cpu(ac->vlan.vlan),
                                                    ac->mac.addr);
                                        break;
                                }
                                spin_lock_bh(&lif->rx_filters.lock);
                                ionic_rx_filter_free(lif, f);
                                spin_unlock_bh(&lif->rx_filters.lock);

                                continue;
                        }

                        /* remove from old id list, save new id in tmp list */
                        spin_lock_bh(&lif->rx_filters.lock);
                        hlist_del(&f->by_id);
                        spin_unlock_bh(&lif->rx_filters.lock);
                        f->filter_id = le32_to_cpu(ctx.comp.rx_filter_add.filter_id);
                        hlist_add_head(&f->by_id, &new_id_list);
                }
        }

        /* rebuild the by_id hash lists with the new filter ids */
        spin_lock_bh(&lif->rx_filters.lock);
        hlist_for_each_entry_safe(f, tmp, &new_id_list, by_id) {
                key = f->filter_id & IONIC_RX_FILTER_HLISTS_MASK;
                head = &lif->rx_filters.by_id[key];
                hlist_add_head(&f->by_id, head);
        }
        spin_unlock_bh(&lif->rx_filters.lock);
}

int ionic_rx_filters_init(struct ionic_lif *lif)
{
        unsigned int i;

        spin_lock_init(&lif->rx_filters.lock);

        spin_lock_bh(&lif->rx_filters.lock);
        for (i = 0; i < IONIC_RX_FILTER_HLISTS; i++) {
                INIT_HLIST_HEAD(&lif->rx_filters.by_hash[i]);
                INIT_HLIST_HEAD(&lif->rx_filters.by_id[i]);
        }
        spin_unlock_bh(&lif->rx_filters.lock);

        return 0;
}

void ionic_rx_filters_deinit(struct ionic_lif *lif)
{
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        struct hlist_node *tmp;
        unsigned int i;

        spin_lock_bh(&lif->rx_filters.lock);
        for (i = 0; i < IONIC_RX_FILTER_HLISTS; i++) {
                head = &lif->rx_filters.by_id[i];
                hlist_for_each_entry_safe(f, tmp, head, by_id)
                        ionic_rx_filter_free(lif, f);
        }
        spin_unlock_bh(&lif->rx_filters.lock);
}

int ionic_rx_filter_save(struct ionic_lif *lif, u32 flow_id, u16 rxq_index,
                         u32 hash, struct ionic_admin_ctx *ctx,
                         enum ionic_filter_state state)
{
        struct device *dev = lif->ionic->dev;
        struct ionic_rx_filter_add_cmd *ac;
        struct ionic_rx_filter *f = NULL;
        struct hlist_head *head;
        unsigned int key;

        ac = &ctx->cmd.rx_filter_add;

        switch (le16_to_cpu(ac->match)) {
        case IONIC_RX_FILTER_MATCH_VLAN:
                key = le16_to_cpu(ac->vlan.vlan);
                f = ionic_rx_filter_by_vlan(lif, le16_to_cpu(ac->vlan.vlan));
                break;
        case IONIC_RX_FILTER_MATCH_MAC:
                key = *(u32 *)ac->mac.addr;
                f = ionic_rx_filter_by_addr(lif, ac->mac.addr);
                break;
        case IONIC_RX_FILTER_MATCH_MAC_VLAN:
                key = le16_to_cpu(ac->mac_vlan.vlan);
                break;
        case IONIC_RX_FILTER_STEER_PKTCLASS:
                key = 0;
                break;
        default:
                return -EINVAL;
        }

        if (f) {
                /* remove from current linking so we can refresh it */
                hlist_del(&f->by_id);
                hlist_del(&f->by_hash);
        } else {
                f = devm_kzalloc(dev, sizeof(*f), GFP_ATOMIC);
                if (!f)
                        return -ENOMEM;
        }

        f->flow_id = flow_id;
        f->filter_id = le32_to_cpu(ctx->comp.rx_filter_add.filter_id);
        f->state = state;
        f->rxq_index = rxq_index;
        memcpy(&f->cmd, ac, sizeof(f->cmd));
        netdev_dbg(lif->netdev, "rx_filter add filter_id %d\n", f->filter_id);

        INIT_HLIST_NODE(&f->by_hash);
        INIT_HLIST_NODE(&f->by_id);

        key = hash_32(key, IONIC_RX_FILTER_HASH_BITS);
        head = &lif->rx_filters.by_hash[key];
        hlist_add_head(&f->by_hash, head);

        key = f->filter_id & IONIC_RX_FILTER_HLISTS_MASK;
        head = &lif->rx_filters.by_id[key];
        hlist_add_head(&f->by_id, head);

        return 0;
}

struct ionic_rx_filter *ionic_rx_filter_by_vlan(struct ionic_lif *lif, u16 vid)
{
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        unsigned int key;

        key = hash_32(vid, IONIC_RX_FILTER_HASH_BITS);
        head = &lif->rx_filters.by_hash[key];

        hlist_for_each_entry(f, head, by_hash) {
                if (le16_to_cpu(f->cmd.match) != IONIC_RX_FILTER_MATCH_VLAN)
                        continue;
                if (le16_to_cpu(f->cmd.vlan.vlan) == vid)
                        return f;
        }

        return NULL;
}

struct ionic_rx_filter *ionic_rx_filter_by_addr(struct ionic_lif *lif,
                                                const u8 *addr)
{
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        unsigned int key;

        key = hash_32(*(u32 *)addr, IONIC_RX_FILTER_HASH_BITS);
        head = &lif->rx_filters.by_hash[key];

        hlist_for_each_entry(f, head, by_hash) {
                if (le16_to_cpu(f->cmd.match) != IONIC_RX_FILTER_MATCH_MAC)
                        continue;
                if (memcmp(addr, f->cmd.mac.addr, ETH_ALEN) == 0)
                        return f;
        }

        return NULL;
}

struct ionic_rx_filter *ionic_rx_filter_rxsteer(struct ionic_lif *lif)
{
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        unsigned int key;

        key = hash_32(0, IONIC_RX_FILTER_HASH_BITS);
        head = &lif->rx_filters.by_hash[key];

        hlist_for_each_entry(f, head, by_hash) {
                if (le16_to_cpu(f->cmd.match) != IONIC_RX_FILTER_STEER_PKTCLASS)
                        continue;
                return f;
        }

        return NULL;
}

static struct ionic_rx_filter *ionic_rx_filter_find(struct ionic_lif *lif,
                                                    struct ionic_rx_filter_add_cmd *ac)
{
        switch (le16_to_cpu(ac->match)) {
        case IONIC_RX_FILTER_MATCH_VLAN:
                return ionic_rx_filter_by_vlan(lif, le16_to_cpu(ac->vlan.vlan));
        case IONIC_RX_FILTER_MATCH_MAC:
                return ionic_rx_filter_by_addr(lif, ac->mac.addr);
        default:
                netdev_err(lif->netdev, "unsupported filter match %d",
                           le16_to_cpu(ac->match));
                return NULL;
        }
}

int ionic_lif_list_addr(struct ionic_lif *lif, const u8 *addr, bool mode)
{
        struct ionic_rx_filter *f;
        int err;

        spin_lock_bh(&lif->rx_filters.lock);

        f = ionic_rx_filter_by_addr(lif, addr);
        if (mode == ADD_ADDR && !f) {
                struct ionic_admin_ctx ctx = {
                        .work = COMPLETION_INITIALIZER_ONSTACK(ctx.work),
                        .cmd.rx_filter_add = {
                                .opcode = IONIC_CMD_RX_FILTER_ADD,
                                .lif_index = cpu_to_le16(lif->index),
                                .match = cpu_to_le16(IONIC_RX_FILTER_MATCH_MAC),
                        },
                };

                memcpy(ctx.cmd.rx_filter_add.mac.addr, addr, ETH_ALEN);
                err = ionic_rx_filter_save(lif, 0, IONIC_RXQ_INDEX_ANY, 0, &ctx,
                                           IONIC_FILTER_STATE_NEW);
                if (err) {
                        spin_unlock_bh(&lif->rx_filters.lock);
                        return err;
                }

        } else if (mode == ADD_ADDR && f) {
                if (f->state == IONIC_FILTER_STATE_OLD)
                        f->state = IONIC_FILTER_STATE_SYNCED;

        } else if (mode == DEL_ADDR && f) {
                if (f->state == IONIC_FILTER_STATE_NEW)
                        ionic_rx_filter_free(lif, f);
                else if (f->state == IONIC_FILTER_STATE_SYNCED)
                        f->state = IONIC_FILTER_STATE_OLD;
        } else if (mode == DEL_ADDR && !f) {
                spin_unlock_bh(&lif->rx_filters.lock);
                return -ENOENT;
        }

        spin_unlock_bh(&lif->rx_filters.lock);

        set_bit(IONIC_LIF_F_FILTER_SYNC_NEEDED, lif->state);

        return 0;
}

static int ionic_lif_filter_add(struct ionic_lif *lif,
                                struct ionic_rx_filter_add_cmd *ac)
{
        struct ionic_admin_ctx ctx = {
                .work = COMPLETION_INITIALIZER_ONSTACK(ctx.work),
        };
        struct ionic_rx_filter *f;
        int nfilters;
        int err = 0;

        ctx.cmd.rx_filter_add = *ac;
        ctx.cmd.rx_filter_add.opcode = IONIC_CMD_RX_FILTER_ADD;
        ctx.cmd.rx_filter_add.lif_index = cpu_to_le16(lif->index);

        spin_lock_bh(&lif->rx_filters.lock);
        f = ionic_rx_filter_find(lif, &ctx.cmd.rx_filter_add);
        if (f) {
                /* don't bother if we already have it and it is sync'd */
                if (f->state == IONIC_FILTER_STATE_SYNCED) {
                        spin_unlock_bh(&lif->rx_filters.lock);
                        return 0;
                }

                /* mark preemptively as sync'd to block any parallel attempts */
                f->state = IONIC_FILTER_STATE_SYNCED;
        } else {
                /* save as SYNCED to catch any DEL requests while processing */
                err = ionic_rx_filter_save(lif, 0, IONIC_RXQ_INDEX_ANY, 0, &ctx,
                                           IONIC_FILTER_STATE_SYNCED);
        }
        spin_unlock_bh(&lif->rx_filters.lock);
        if (err)
                return err;

        /* Don't bother with the write to FW if we know there's no room,
         * we can try again on the next sync attempt.
         * Since the FW doesn't have a way to tell us the vlan limit,
         * we start max_vlans at 0 until we hit the ENOSPC error.
         */
        switch (le16_to_cpu(ctx.cmd.rx_filter_add.match)) {
        case IONIC_RX_FILTER_MATCH_VLAN:
                netdev_dbg(lif->netdev, "%s: rx_filter add VLAN %d\n",
                           __func__, ctx.cmd.rx_filter_add.vlan.vlan);
                if (lif->max_vlans && lif->nvlans >= lif->max_vlans)
                        err = -ENOSPC;
                break;
        case IONIC_RX_FILTER_MATCH_MAC:
                netdev_dbg(lif->netdev, "%s: rx_filter add ADDR %pM\n",
                           __func__, ctx.cmd.rx_filter_add.mac.addr);
                nfilters = le32_to_cpu(lif->identity->eth.max_ucast_filters);
                if ((lif->nucast + lif->nmcast) >= nfilters)
                        err = -ENOSPC;
                break;
        }

        if (err != -ENOSPC)
                err = ionic_adminq_post_wait_nomsg(lif, &ctx);

        spin_lock_bh(&lif->rx_filters.lock);

        if (err && err != -EEXIST) {
                /* set the state back to NEW so we can try again later */
                f = ionic_rx_filter_find(lif, &ctx.cmd.rx_filter_add);
                if (f && f->state == IONIC_FILTER_STATE_SYNCED) {
                        f->state = IONIC_FILTER_STATE_NEW;

                        /* If -ENOSPC we won't waste time trying to sync again
                         * until there is a delete that might make room
                         */
                        if (err != -ENOSPC)
                                set_bit(IONIC_LIF_F_FILTER_SYNC_NEEDED, lif->state);
                }

                spin_unlock_bh(&lif->rx_filters.lock);

                /* store the max_vlans limit that we found */
                if (err == -ENOSPC &&
                    le16_to_cpu(ctx.cmd.rx_filter_add.match) == IONIC_RX_FILTER_MATCH_VLAN)
                        lif->max_vlans = lif->nvlans;

                /* Prevent unnecessary error messages on recoverable
                 * errors as the filter will get retried on the next
                 * sync attempt.
                 */
                switch (err) {
                case -ENOSPC:
                case -ENXIO:
                case -ETIMEDOUT:
                case -EAGAIN:
                case -EBUSY:
                        return 0;
                default:
                        break;
                }

                ionic_adminq_netdev_err_print(lif, ctx.cmd.cmd.opcode,
                                              ctx.comp.comp.status, err);
                switch (le16_to_cpu(ctx.cmd.rx_filter_add.match)) {
                case IONIC_RX_FILTER_MATCH_VLAN:
                        netdev_info(lif->netdev, "rx_filter add failed: VLAN %d\n",
                                    ctx.cmd.rx_filter_add.vlan.vlan);
                        break;
                case IONIC_RX_FILTER_MATCH_MAC:
                        netdev_info(lif->netdev, "rx_filter add failed: ADDR %pM\n",
                                    ctx.cmd.rx_filter_add.mac.addr);
                        break;
                }

                return err;
        }

        switch (le16_to_cpu(ctx.cmd.rx_filter_add.match)) {
        case IONIC_RX_FILTER_MATCH_VLAN:
                lif->nvlans++;
                break;
        case IONIC_RX_FILTER_MATCH_MAC:
                if (is_multicast_ether_addr(ctx.cmd.rx_filter_add.mac.addr))
                        lif->nmcast++;
                else
                        lif->nucast++;
                break;
        }

        f = ionic_rx_filter_find(lif, &ctx.cmd.rx_filter_add);
        if (f && f->state == IONIC_FILTER_STATE_OLD) {
                /* Someone requested a delete while we were adding
                 * so update the filter info with the results from the add
                 * and the data will be there for the delete on the next
                 * sync cycle.
                 */
                err = ionic_rx_filter_save(lif, 0, IONIC_RXQ_INDEX_ANY, 0, &ctx,
                                           IONIC_FILTER_STATE_OLD);
        } else {
                err = ionic_rx_filter_save(lif, 0, IONIC_RXQ_INDEX_ANY, 0, &ctx,
                                           IONIC_FILTER_STATE_SYNCED);
        }

        spin_unlock_bh(&lif->rx_filters.lock);

        return err;
}

int ionic_lif_addr_add(struct ionic_lif *lif, const u8 *addr)
{
        struct ionic_rx_filter_add_cmd ac = {
                .match = cpu_to_le16(IONIC_RX_FILTER_MATCH_MAC),
        };

        memcpy(&ac.mac.addr, addr, ETH_ALEN);

        return ionic_lif_filter_add(lif, &ac);
}

int ionic_lif_vlan_add(struct ionic_lif *lif, const u16 vid)
{
        struct ionic_rx_filter_add_cmd ac = {
                .match = cpu_to_le16(IONIC_RX_FILTER_MATCH_VLAN),
                .vlan.vlan = cpu_to_le16(vid),
        };

        return ionic_lif_filter_add(lif, &ac);
}

static int ionic_lif_filter_del(struct ionic_lif *lif,
                                struct ionic_rx_filter_add_cmd *ac)
{
        struct ionic_admin_ctx ctx = {
                .work = COMPLETION_INITIALIZER_ONSTACK(ctx.work),
                .cmd.rx_filter_del = {
                        .opcode = IONIC_CMD_RX_FILTER_DEL,
                        .lif_index = cpu_to_le16(lif->index),
                },
        };
        struct ionic_rx_filter *f;
        int state;
        int err;

        spin_lock_bh(&lif->rx_filters.lock);
        f = ionic_rx_filter_find(lif, ac);
        if (!f) {
                spin_unlock_bh(&lif->rx_filters.lock);
                return -ENOENT;
        }

        switch (le16_to_cpu(ac->match)) {
        case IONIC_RX_FILTER_MATCH_VLAN:
                netdev_dbg(lif->netdev, "%s: rx_filter del VLAN %d id %d\n",
                           __func__, ac->vlan.vlan, f->filter_id);
                lif->nvlans--;
                break;
        case IONIC_RX_FILTER_MATCH_MAC:
                netdev_dbg(lif->netdev, "%s: rx_filter del ADDR %pM id %d\n",
                           __func__, ac->mac.addr, f->filter_id);
                if (is_multicast_ether_addr(ac->mac.addr) && lif->nmcast)
                        lif->nmcast--;
                else if (!is_multicast_ether_addr(ac->mac.addr) && lif->nucast)
                        lif->nucast--;
                break;
        }

        state = f->state;
        ctx.cmd.rx_filter_del.filter_id = cpu_to_le32(f->filter_id);
        ionic_rx_filter_free(lif, f);

        spin_unlock_bh(&lif->rx_filters.lock);

        if (state != IONIC_FILTER_STATE_NEW) {
                err = ionic_adminq_post_wait_nomsg(lif, &ctx);

                switch (err) {
                        /* ignore these errors */
                case -EEXIST:
                case -ENXIO:
                case -ETIMEDOUT:
                case -EAGAIN:
                case -EBUSY:
                case 0:
                        break;
                default:
                        ionic_adminq_netdev_err_print(lif, ctx.cmd.cmd.opcode,
                                                      ctx.comp.comp.status, err);
                        return err;
                }
        }

        return 0;
}

int ionic_lif_addr_del(struct ionic_lif *lif, const u8 *addr)
{
        struct ionic_rx_filter_add_cmd ac = {
                .match = cpu_to_le16(IONIC_RX_FILTER_MATCH_MAC),
        };

        memcpy(&ac.mac.addr, addr, ETH_ALEN);

        return ionic_lif_filter_del(lif, &ac);
}

int ionic_lif_vlan_del(struct ionic_lif *lif, const u16 vid)
{
        struct ionic_rx_filter_add_cmd ac = {
                .match = cpu_to_le16(IONIC_RX_FILTER_MATCH_VLAN),
                .vlan.vlan = cpu_to_le16(vid),
        };

        return ionic_lif_filter_del(lif, &ac);
}

struct sync_item {
        struct list_head list;
        struct ionic_rx_filter f;
};

void ionic_rx_filter_sync(struct ionic_lif *lif)
{
        struct device *dev = lif->ionic->dev;
        struct list_head sync_add_list;
        struct list_head sync_del_list;
        struct sync_item *sync_item;
        struct ionic_rx_filter *f;
        struct hlist_head *head;
        struct hlist_node *tmp;
        struct sync_item *spos;
        unsigned int i;

        INIT_LIST_HEAD(&sync_add_list);
        INIT_LIST_HEAD(&sync_del_list);

        clear_bit(IONIC_LIF_F_FILTER_SYNC_NEEDED, lif->state);

        /* Copy the filters to be added and deleted
         * into a separate local list that needs no locking.
         */
        spin_lock_bh(&lif->rx_filters.lock);
        for (i = 0; i < IONIC_RX_FILTER_HLISTS; i++) {
                head = &lif->rx_filters.by_id[i];
                hlist_for_each_entry_safe(f, tmp, head, by_id) {
                        if (f->state == IONIC_FILTER_STATE_NEW ||
                            f->state == IONIC_FILTER_STATE_OLD) {
                                sync_item = devm_kzalloc(dev, sizeof(*sync_item),
                                                         GFP_ATOMIC);
                                if (!sync_item)
                                        goto loop_out;

                                sync_item->f = *f;

                                if (f->state == IONIC_FILTER_STATE_NEW)
                                        list_add(&sync_item->list, &sync_add_list);
                                else
                                        list_add(&sync_item->list, &sync_del_list);
                        }
                }
        }
loop_out:
        spin_unlock_bh(&lif->rx_filters.lock);

        /* If the add or delete fails, it won't get marked as sync'd
         * and will be tried again in the next sync action.
         * Do the deletes first in case we're in an overflow state and
         * they can clear room for some new filters
         */
        list_for_each_entry_safe(sync_item, spos, &sync_del_list, list) {
                ionic_lif_filter_del(lif, &sync_item->f.cmd);

                list_del(&sync_item->list);
                devm_kfree(dev, sync_item);
        }

        list_for_each_entry_safe(sync_item, spos, &sync_add_list, list) {
                ionic_lif_filter_add(lif, &sync_item->f.cmd);

                list_del(&sync_item->list);
                devm_kfree(dev, sync_item);
        }
}