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

#include <linux/phy.h>
#include <linux/phylib_stubs.h>

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

struct linkstate_req_info {
        struct ethnl_req_info           base;
};

struct linkstate_reply_data {
        struct ethnl_reply_data                 base;
        int                                     link;
        int                                     sqi;
        int                                     sqi_max;
        struct ethtool_link_ext_stats           link_stats;
        bool                                    link_ext_state_provided;
        struct ethtool_link_ext_state_info      ethtool_link_ext_state_info;
};

#define LINKSTATE_REPDATA(__reply_base) \
        container_of(__reply_base, struct linkstate_reply_data, base)

const struct nla_policy ethnl_linkstate_get_policy[] = {
        [ETHTOOL_A_LINKSTATE_HEADER]            =
                NLA_POLICY_NESTED(ethnl_header_policy_stats),
};

static int linkstate_get_sqi(struct phy_device *phydev)
{
        int ret;

        if (!phydev)
                return -EOPNOTSUPP;

        mutex_lock(&phydev->lock);
        if (!phydev->drv || !phydev->drv->get_sqi)
                ret = -EOPNOTSUPP;
        else if (!phydev->link)
                ret = -ENETDOWN;
        else
                ret = phydev->drv->get_sqi(phydev);
        mutex_unlock(&phydev->lock);

        return ret;
}

static int linkstate_get_sqi_max(struct phy_device *phydev)
{
        int ret;

        if (!phydev)
                return -EOPNOTSUPP;

        mutex_lock(&phydev->lock);
        if (!phydev->drv || !phydev->drv->get_sqi_max)
                ret = -EOPNOTSUPP;
        else if (!phydev->link)
                ret = -ENETDOWN;
        else
                ret = phydev->drv->get_sqi_max(phydev);
        mutex_unlock(&phydev->lock);

        return ret;
};

static bool linkstate_sqi_critical_error(int sqi)
{
        return sqi < 0 && sqi != -EOPNOTSUPP && sqi != -ENETDOWN;
}

static bool linkstate_sqi_valid(struct linkstate_reply_data *data)
{
        return data->sqi >= 0 && data->sqi_max >= 0 &&
               data->sqi <= data->sqi_max;
}

static int linkstate_get_link_ext_state(struct net_device *dev,
                                        struct linkstate_reply_data *data)
{
        int err;

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

        err = dev->ethtool_ops->get_link_ext_state(dev, &data->ethtool_link_ext_state_info);
        if (err)
                return err;

        data->link_ext_state_provided = true;

        return 0;
}

static int linkstate_prepare_data(const struct ethnl_req_info *req_base,
                                  struct ethnl_reply_data *reply_base,
                                  const struct genl_info *info)
{
        struct linkstate_reply_data *data = LINKSTATE_REPDATA(reply_base);
        struct net_device *dev = reply_base->dev;
        struct nlattr **tb = info->attrs;
        struct phy_device *phydev;
        int ret;

        phydev = ethnl_req_get_phydev(req_base, tb, ETHTOOL_A_LINKSTATE_HEADER,
                                      info->extack);
        if (IS_ERR(phydev))
                return PTR_ERR(phydev);

        ret = ethnl_ops_begin(dev);
        if (ret < 0)
                return ret;
        data->link = __ethtool_get_link(dev);

        ret = linkstate_get_sqi(phydev);
        if (linkstate_sqi_critical_error(ret))
                goto out;
        data->sqi = ret;

        ret = linkstate_get_sqi_max(phydev);
        if (linkstate_sqi_critical_error(ret))
                goto out;
        data->sqi_max = ret;

        if (dev->flags & IFF_UP) {
                ret = linkstate_get_link_ext_state(dev, data);
                if (ret < 0 && ret != -EOPNOTSUPP && ret != -ENODATA)
                        goto out;
        }

        ethtool_stats_init((u64 *)&data->link_stats,
                           sizeof(data->link_stats) / 8);

        if (req_base->flags & ETHTOOL_FLAG_STATS) {
                if (phydev)
                        phy_ethtool_get_link_ext_stats(phydev,
                                                       &data->link_stats);

                if (dev->ethtool_ops->get_link_ext_stats)
                        dev->ethtool_ops->get_link_ext_stats(dev,
                                                             &data->link_stats);
        }

        ret = 0;
out:
        ethnl_ops_complete(dev);
        return ret;
}

static int linkstate_reply_size(const struct ethnl_req_info *req_base,
                                const struct ethnl_reply_data *reply_base)
{
        struct linkstate_reply_data *data = LINKSTATE_REPDATA(reply_base);
        int len;

        len = nla_total_size(sizeof(u8)) /* LINKSTATE_LINK */
                + 0;

        if (linkstate_sqi_valid(data)) {
                len += nla_total_size(sizeof(u32)); /* LINKSTATE_SQI */
                len += nla_total_size(sizeof(u32)); /* LINKSTATE_SQI_MAX */
        }

        if (data->link_ext_state_provided)
                len += nla_total_size(sizeof(u8)); /* LINKSTATE_EXT_STATE */

        if (data->ethtool_link_ext_state_info.__link_ext_substate)
                len += nla_total_size(sizeof(u8)); /* LINKSTATE_EXT_SUBSTATE */

        if (data->link_stats.link_down_events != ETHTOOL_STAT_NOT_SET)
                len += nla_total_size(sizeof(u32));

        return len;
}

static int linkstate_fill_reply(struct sk_buff *skb,
                                const struct ethnl_req_info *req_base,
                                const struct ethnl_reply_data *reply_base)
{
        struct linkstate_reply_data *data = LINKSTATE_REPDATA(reply_base);

        if (data->link >= 0 &&
            nla_put_u8(skb, ETHTOOL_A_LINKSTATE_LINK, !!data->link))
                return -EMSGSIZE;

        if (linkstate_sqi_valid(data)) {
                if (nla_put_u32(skb, ETHTOOL_A_LINKSTATE_SQI, data->sqi))
                        return -EMSGSIZE;

                if (nla_put_u32(skb, ETHTOOL_A_LINKSTATE_SQI_MAX,
                                data->sqi_max))
                        return -EMSGSIZE;
        }

        if (data->link_ext_state_provided) {
                if (nla_put_u8(skb, ETHTOOL_A_LINKSTATE_EXT_STATE,
                               data->ethtool_link_ext_state_info.link_ext_state))
                        return -EMSGSIZE;

                if (data->ethtool_link_ext_state_info.__link_ext_substate &&
                    nla_put_u8(skb, ETHTOOL_A_LINKSTATE_EXT_SUBSTATE,
                               data->ethtool_link_ext_state_info.__link_ext_substate))
                        return -EMSGSIZE;
        }

        if (data->link_stats.link_down_events != ETHTOOL_STAT_NOT_SET)
                if (nla_put_u32(skb, ETHTOOL_A_LINKSTATE_EXT_DOWN_CNT,
                                data->link_stats.link_down_events))
                        return -EMSGSIZE;

        return 0;
}

const struct ethnl_request_ops ethnl_linkstate_request_ops = {
        .request_cmd            = ETHTOOL_MSG_LINKSTATE_GET,
        .reply_cmd              = ETHTOOL_MSG_LINKSTATE_GET_REPLY,
        .hdr_attr               = ETHTOOL_A_LINKSTATE_HEADER,
        .req_info_size          = sizeof(struct linkstate_req_info),
        .reply_data_size        = sizeof(struct linkstate_reply_data),

        .prepare_data           = linkstate_prepare_data,
        .reply_size             = linkstate_reply_size,
        .fill_reply             = linkstate_fill_reply,
};