root/net/ethtool/pause.c
// SPDX-License-Identifier: GPL-2.0-only

#include "netlink.h"
#include "common.h"

struct pause_req_info {
        struct ethnl_req_info           base;
        enum ethtool_mac_stats_src      src;
};

#define PAUSE_REQINFO(__req_base) \
        container_of(__req_base, struct pause_req_info, base)

struct pause_reply_data {
        struct ethnl_reply_data         base;
        struct ethtool_pauseparam       pauseparam;
        struct ethtool_pause_stats      pausestat;
};

#define PAUSE_REPDATA(__reply_base) \
        container_of(__reply_base, struct pause_reply_data, base)

const struct nla_policy ethnl_pause_get_policy[] = {
        [ETHTOOL_A_PAUSE_HEADER]                =
                NLA_POLICY_NESTED(ethnl_header_policy_stats),
        [ETHTOOL_A_PAUSE_STATS_SRC]             =
                NLA_POLICY_MAX(NLA_U32, ETHTOOL_MAC_STATS_SRC_PMAC),
};

static int pause_parse_request(struct ethnl_req_info *req_base,
                               struct nlattr **tb,
                               struct netlink_ext_ack *extack)
{
        enum ethtool_mac_stats_src src = ETHTOOL_MAC_STATS_SRC_AGGREGATE;
        struct pause_req_info *req_info = PAUSE_REQINFO(req_base);

        if (tb[ETHTOOL_A_PAUSE_STATS_SRC]) {
                if (!(req_base->flags & ETHTOOL_FLAG_STATS)) {
                        NL_SET_ERR_MSG_MOD(extack,
                                           "ETHTOOL_FLAG_STATS must be set when requesting a source of stats");
                        return -EINVAL;
                }

                src = nla_get_u32(tb[ETHTOOL_A_PAUSE_STATS_SRC]);
        }

        req_info->src = src;

        return 0;
}

static int pause_prepare_data(const struct ethnl_req_info *req_base,
                              struct ethnl_reply_data *reply_base,
                              const struct genl_info *info)
{
        const struct pause_req_info *req_info = PAUSE_REQINFO(req_base);
        struct pause_reply_data *data = PAUSE_REPDATA(reply_base);
        enum ethtool_mac_stats_src src = req_info->src;
        struct net_device *dev = reply_base->dev;
        int ret;

        if (!dev->ethtool_ops->get_pauseparam)
                return -EOPNOTSUPP;

        ethtool_stats_init((u64 *)&data->pausestat,
                           sizeof(data->pausestat) / 8);
        data->pausestat.src = src;

        ret = ethnl_ops_begin(dev);
        if (ret < 0)
                return ret;

        if ((src == ETHTOOL_MAC_STATS_SRC_EMAC ||
             src == ETHTOOL_MAC_STATS_SRC_PMAC) &&
            !__ethtool_dev_mm_supported(dev)) {
                NL_SET_ERR_MSG_MOD(info->extack,
                                   "Device does not support MAC merge layer");
                ethnl_ops_complete(dev);
                return -EOPNOTSUPP;
        }

        dev->ethtool_ops->get_pauseparam(dev, &data->pauseparam);
        if (req_base->flags & ETHTOOL_FLAG_STATS &&
            dev->ethtool_ops->get_pause_stats)
                dev->ethtool_ops->get_pause_stats(dev, &data->pausestat);

        ethnl_ops_complete(dev);

        return 0;
}

static int pause_reply_size(const struct ethnl_req_info *req_base,
                            const struct ethnl_reply_data *reply_base)
{
        int n = nla_total_size(sizeof(u8)) +    /* _PAUSE_AUTONEG */
                nla_total_size(sizeof(u8)) +    /* _PAUSE_RX */
                nla_total_size(sizeof(u8));     /* _PAUSE_TX */

        if (req_base->flags & ETHTOOL_FLAG_STATS)
                n += nla_total_size(0) +        /* _PAUSE_STATS */
                     nla_total_size(sizeof(u32)) + /* _PAUSE_STATS_SRC */
                     nla_total_size_64bit(sizeof(u64)) * ETHTOOL_PAUSE_STAT_CNT;
        return n;
}

static int ethtool_put_stat(struct sk_buff *skb, u64 val, u16 attrtype,
                            u16 padtype)
{
        if (val == ETHTOOL_STAT_NOT_SET)
                return 0;
        if (nla_put_u64_64bit(skb, attrtype, val, padtype))
                return -EMSGSIZE;

        return 0;
}

static int pause_put_stats(struct sk_buff *skb,
                           const struct ethtool_pause_stats *pause_stats)
{
        const u16 pad = ETHTOOL_A_PAUSE_STAT_PAD;
        struct nlattr *nest;

        if (nla_put_u32(skb, ETHTOOL_A_PAUSE_STATS_SRC, pause_stats->src))
                return -EMSGSIZE;

        nest = nla_nest_start(skb, ETHTOOL_A_PAUSE_STATS);
        if (!nest)
                return -EMSGSIZE;

        if (ethtool_put_stat(skb, pause_stats->tx_pause_frames,
                             ETHTOOL_A_PAUSE_STAT_TX_FRAMES, pad) ||
            ethtool_put_stat(skb, pause_stats->rx_pause_frames,
                             ETHTOOL_A_PAUSE_STAT_RX_FRAMES, pad))
                goto err_cancel;

        nla_nest_end(skb, nest);
        return 0;

err_cancel:
        nla_nest_cancel(skb, nest);
        return -EMSGSIZE;
}

static int pause_fill_reply(struct sk_buff *skb,
                            const struct ethnl_req_info *req_base,
                            const struct ethnl_reply_data *reply_base)
{
        const struct pause_reply_data *data = PAUSE_REPDATA(reply_base);
        const struct ethtool_pauseparam *pauseparam = &data->pauseparam;

        if (nla_put_u8(skb, ETHTOOL_A_PAUSE_AUTONEG, !!pauseparam->autoneg) ||
            nla_put_u8(skb, ETHTOOL_A_PAUSE_RX, !!pauseparam->rx_pause) ||
            nla_put_u8(skb, ETHTOOL_A_PAUSE_TX, !!pauseparam->tx_pause))
                return -EMSGSIZE;

        if (req_base->flags & ETHTOOL_FLAG_STATS &&
            pause_put_stats(skb, &data->pausestat))
                return -EMSGSIZE;

        return 0;
}

/* PAUSE_SET */

const struct nla_policy ethnl_pause_set_policy[] = {
        [ETHTOOL_A_PAUSE_HEADER]                =
                NLA_POLICY_NESTED(ethnl_header_policy),
        [ETHTOOL_A_PAUSE_AUTONEG]               = { .type = NLA_U8 },
        [ETHTOOL_A_PAUSE_RX]                    = { .type = NLA_U8 },
        [ETHTOOL_A_PAUSE_TX]                    = { .type = NLA_U8 },
        [ETHTOOL_A_PAUSE_STATS_SRC]             = { .type = NLA_REJECT },
};

static int
ethnl_set_pause_validate(struct ethnl_req_info *req_info,
                         struct genl_info *info)
{
        const struct ethtool_ops *ops = req_info->dev->ethtool_ops;

        return ops->get_pauseparam && ops->set_pauseparam ? 1 : -EOPNOTSUPP;
}

static int
ethnl_set_pause(struct ethnl_req_info *req_info, struct genl_info *info)
{
        struct net_device *dev = req_info->dev;
        struct ethtool_pauseparam params = {};
        struct nlattr **tb = info->attrs;
        bool mod = false;
        int ret;

        dev->ethtool_ops->get_pauseparam(dev, &params);

        ethnl_update_bool32(&params.autoneg, tb[ETHTOOL_A_PAUSE_AUTONEG], &mod);
        ethnl_update_bool32(&params.rx_pause, tb[ETHTOOL_A_PAUSE_RX], &mod);
        ethnl_update_bool32(&params.tx_pause, tb[ETHTOOL_A_PAUSE_TX], &mod);
        if (!mod)
                return 0;

        ret = dev->ethtool_ops->set_pauseparam(dev, &params);
        return ret < 0 ? ret : 1;
}

const struct ethnl_request_ops ethnl_pause_request_ops = {
        .request_cmd            = ETHTOOL_MSG_PAUSE_GET,
        .reply_cmd              = ETHTOOL_MSG_PAUSE_GET_REPLY,
        .hdr_attr               = ETHTOOL_A_PAUSE_HEADER,
        .req_info_size          = sizeof(struct pause_req_info),
        .reply_data_size        = sizeof(struct pause_reply_data),

        .parse_request          = pause_parse_request,
        .prepare_data           = pause_prepare_data,
        .reply_size             = pause_reply_size,
        .fill_reply             = pause_fill_reply,

        .set_validate           = ethnl_set_pause_validate,
        .set                    = ethnl_set_pause,
        .set_ntf_cmd            = ETHTOOL_MSG_PAUSE_NTF,
};