/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <linux/ethtool.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <sys/ioctl.h>

#include "alloc-util.h"
#include "conf-parser.h"
#include "errno-util.h"
#include "ether-addr-util.h"
#include "ethtool-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "log.h"
#include "macro-fundamental.h"
#include "memory-util.h"
#include "parse-util.h"
#include "socket-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
#include "strxcpyx.h"
#include "time-util.h"

static const char* const duplex_table[_DUP_MAX] = {
        [DUP_FULL] = "full",
        [DUP_HALF] = "half"
};

DEFINE_STRING_TABLE_LOOKUP(duplex, Duplex);
DEFINE_CONFIG_PARSE_ENUM(config_parse_duplex, duplex, Duplex);

static const struct {
        uint32_t opt;
        const char *name;
} wol_option_map[] = {
        { WAKE_PHY,         "phy"        },
        { WAKE_UCAST,       "unicast",   },
        { WAKE_MCAST,       "multicast", },
        { WAKE_BCAST,       "broadcast", },
        { WAKE_ARP,         "arp",       },
        { WAKE_MAGIC,       "magic",     },
        { WAKE_MAGICSECURE, "secureon",  },
};

int wol_options_to_string_alloc(uint32_t opts, char **ret) {
        _cleanup_free_ char *str = NULL;

        assert(ret);

        if (opts == UINT32_MAX) {
                *ret = NULL;
                return 0;
        }

        FOREACH_ELEMENT(option, wol_option_map)
                if (opts & option->opt &&
                    !strextend_with_separator(&str, ",", option->name))
                        return -ENOMEM;

        if (!str) {
                str = strdup("off");
                if (!str)
                        return -ENOMEM;
        }

        *ret = TAKE_PTR(str);
        return 1;
}

static const char* const port_table[] = {
        [NET_DEV_PORT_TP]     = "tp",
        [NET_DEV_PORT_AUI]    = "aui",
        [NET_DEV_PORT_MII]    = "mii",
        [NET_DEV_PORT_FIBRE]  = "fibre",
        [NET_DEV_PORT_BNC]    = "bnc",
};

DEFINE_STRING_TABLE_LOOKUP(port, NetDevPort);
DEFINE_CONFIG_PARSE_ENUM(config_parse_port, port, NetDevPort);

static const char* const mdi_table[] = {
        [ETH_TP_MDI_INVALID]  = "unknown",
        [ETH_TP_MDI]          = "mdi",
        [ETH_TP_MDI_X]        = "mdi-x",
        [ETH_TP_MDI_AUTO]     = "auto",
};

DEFINE_STRING_TABLE_LOOKUP_TO_STRING(mdi, int);

static const char* const netdev_feature_table[_NET_DEV_FEAT_MAX] = {
        [NET_DEV_FEAT_SG]                  = "tx-scatter-gather",
        [NET_DEV_FEAT_IP_CSUM]             = "tx-checksum-ipv4",
        [NET_DEV_FEAT_HW_CSUM]             = "tx-checksum-ip-generic",
        [NET_DEV_FEAT_IPV6_CSUM]           = "tx-checksum-ipv6",
        [NET_DEV_FEAT_HIGHDMA]             = "highdma",
        [NET_DEV_FEAT_FRAGLIST]            = "tx-scatter-gather-fraglist",
        [NET_DEV_FEAT_HW_VLAN_CTAG_TX]     = "tx-vlan-hw-insert",
        [NET_DEV_FEAT_HW_VLAN_CTAG_RX]     = "rx-vlan-hw-parse",
        [NET_DEV_FEAT_HW_VLAN_CTAG_FILTER] = "rx-vlan-filter",
        [NET_DEV_FEAT_HW_VLAN_STAG_TX]     = "tx-vlan-stag-hw-insert",
        [NET_DEV_FEAT_HW_VLAN_STAG_RX]     = "rx-vlan-stag-hw-parse",
        [NET_DEV_FEAT_HW_VLAN_STAG_FILTER] = "rx-vlan-stag-filter",
        [NET_DEV_FEAT_VLAN_CHALLENGED]     = "vlan-challenged",
        [NET_DEV_FEAT_GSO]                 = "tx-generic-segmentation",
        [NET_DEV_FEAT_LLTX]                = "tx-lockless",
        [NET_DEV_FEAT_NETNS_LOCAL]         = "netns-local",
        [NET_DEV_FEAT_GRO]                 = "rx-gro",
        [NET_DEV_FEAT_GRO_HW]              = "rx-gro-hw",
        [NET_DEV_FEAT_LRO]                 = "rx-lro",
        [NET_DEV_FEAT_TSO]                 = "tx-tcp-segmentation",
        [NET_DEV_FEAT_GSO_ROBUST]          = "tx-gso-robust",
        [NET_DEV_FEAT_TSO_ECN]             = "tx-tcp-ecn-segmentation",
        [NET_DEV_FEAT_TSO_MANGLEID]        = "tx-tcp-mangleid-segmentation",
        [NET_DEV_FEAT_TSO6]                = "tx-tcp6-segmentation",
        [NET_DEV_FEAT_FSO]                 = "tx-fcoe-segmentation",
        [NET_DEV_FEAT_GSO_GRE]             = "tx-gre-segmentation",
        [NET_DEV_FEAT_GSO_GRE_CSUM]        = "tx-gre-csum-segmentation",
        [NET_DEV_FEAT_GSO_IPXIP4]          = "tx-ipxip4-segmentation",
        [NET_DEV_FEAT_GSO_IPXIP6]          = "tx-ipxip6-segmentation",
        [NET_DEV_FEAT_GSO_UDP_TUNNEL]      = "tx-udp_tnl-segmentation",
        [NET_DEV_FEAT_GSO_UDP_TUNNEL_CSUM] = "tx-udp_tnl-csum-segmentation",
        [NET_DEV_FEAT_GSO_PARTIAL]         = "tx-gso-partial",
        [NET_DEV_FEAT_GSO_TUNNEL_REMCSUM]  = "tx-tunnel-remcsum-segmentation",
        [NET_DEV_FEAT_GSO_SCTP]            = "tx-sctp-segmentation",
        [NET_DEV_FEAT_GSO_ESP]             = "tx-esp-segmentation",
        [NET_DEV_FEAT_GSO_UDP_L4]          = "tx-udp-segmentation",
        [NET_DEV_FEAT_GSO_FRAGLIST]        = "tx-gso-list",
        [NET_DEV_FEAT_FCOE_CRC]            = "tx-checksum-fcoe-crc",
        [NET_DEV_FEAT_SCTP_CRC]            = "tx-checksum-sctp",
        [NET_DEV_FEAT_FCOE_MTU]            = "fcoe-mtu",
        [NET_DEV_FEAT_NTUPLE]              = "rx-ntuple-filter",
        [NET_DEV_FEAT_RXHASH]              = "rx-hashing",
        [NET_DEV_FEAT_RXCSUM]              = "rx-checksum",
        [NET_DEV_FEAT_NOCACHE_COPY]        = "tx-nocache-copy",
        [NET_DEV_FEAT_LOOPBACK]            = "loopback",
        [NET_DEV_FEAT_RXFCS]               = "rx-fcs",
        [NET_DEV_FEAT_RXALL]               = "rx-all",
        [NET_DEV_FEAT_HW_L2FW_DOFFLOAD]    = "l2-fwd-offload",
        [NET_DEV_FEAT_HW_TC]               = "hw-tc-offload",
        [NET_DEV_FEAT_HW_ESP]              = "esp-hw-offload",
        [NET_DEV_FEAT_HW_ESP_TX_CSUM]      = "esp-tx-csum-hw-offload",
        [NET_DEV_FEAT_RX_UDP_TUNNEL_PORT]  = "rx-udp_tunnel-port-offload",
        [NET_DEV_FEAT_HW_TLS_RECORD]       = "tls-hw-record",
        [NET_DEV_FEAT_HW_TLS_TX]           = "tls-hw-tx-offload",
        [NET_DEV_FEAT_HW_TLS_RX]           = "tls-hw-rx-offload",
        [NET_DEV_FEAT_GRO_FRAGLIST]        = "rx-gro-list",
        [NET_DEV_FEAT_HW_MACSEC]           = "macsec-hw-offload",
        [NET_DEV_FEAT_GRO_UDP_FWD]         = "rx-udp-gro-forwarding",
        [NET_DEV_FEAT_HW_HSR_TAG_INS]      = "hsr-tag-ins-offload",
        [NET_DEV_FEAT_HW_HSR_TAG_RM]       = "hsr-tag-rm-offload",
        [NET_DEV_FEAT_HW_HSR_FWD]          = "hsr-fwd-offload",
        [NET_DEV_FEAT_HW_HSR_DUP]          = "hsr-dup-offload",

        [NET_DEV_FEAT_TXCSUM]              = "tx-checksum-", /* The suffix "-" means any feature beginning with "tx-checksum-" */
};

static const char* const ethtool_link_mode_bit_table[] = {
#  include "ethtool-link-mode.inc"
};
/* Make sure the array is large enough to fit all bits */
assert_cc((ELEMENTSOF(ethtool_link_mode_bit_table)-1) / 32 < N_ADVERTISE);

DEFINE_STRING_TABLE_LOOKUP(ethtool_link_mode_bit, enum ethtool_link_mode_bit_indices);

static int ethtool_connect(int *ethtool_fd) {
        int fd;

        assert(ethtool_fd);

        /* This does nothing if already connected. */
        if (*ethtool_fd >= 0)
                return 0;

        fd = socket_ioctl_fd();
        if (fd < 0)
                return log_debug_errno(fd, "ethtool: could not create control socket: %m");

        *ethtool_fd = fd;
        return 0;
}

int ethtool_get_driver(int *ethtool_fd, const char *ifname, char **ret) {
        struct ethtool_drvinfo ecmd = {
                .cmd = ETHTOOL_GDRVINFO,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        int r;

        assert(ethtool_fd);
        assert(ifname);
        assert(ret);

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (isempty(ecmd.driver))
                return -ENODATA;

        return strdup_to(ret, ecmd.driver);
}

int ethtool_get_link_info(
                int *ethtool_fd,
                const char *ifname,
                int *ret_autonegotiation,
                uint64_t *ret_speed,
                Duplex *ret_duplex,
                NetDevPort *ret_port) {

        struct ethtool_cmd ecmd = {
                .cmd = ETHTOOL_GSET,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        int r;

        assert(ethtool_fd);
        assert(ifname);

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (ret_autonegotiation)
                *ret_autonegotiation = ecmd.autoneg;

        if (ret_speed) {
                uint32_t speed;

                speed = ethtool_cmd_speed(&ecmd);
                *ret_speed = speed == (uint32_t) SPEED_UNKNOWN ?
                        UINT64_MAX : (uint64_t) speed * 1000 * 1000;
        }

        if (ret_duplex)
                *ret_duplex = ecmd.duplex;

        if (ret_port)
                *ret_port = ecmd.port;

        return 0;
}

int ethtool_get_permanent_hw_addr(int *ethtool_fd, const char *ifname, struct hw_addr_data *ret) {
        _cleanup_close_ int fd = -EBADF;
        union {
                struct ethtool_perm_addr addr;
                uint8_t buf[offsetof(struct ethtool_perm_addr, data) + HW_ADDR_MAX_SIZE];
        } epaddr = {
                .addr.cmd = ETHTOOL_GPERMADDR,
                .addr.size = HW_ADDR_MAX_SIZE,
        };
        struct ifreq ifr = {
                .ifr_data = (caddr_t) &epaddr,
        };
        int r;

        assert(ifname);
        assert(ret);

        if (!ethtool_fd)
                ethtool_fd = &fd;
        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (epaddr.addr.size == 0)
                return -ENODATA;

        if (epaddr.addr.size > HW_ADDR_MAX_SIZE)
                return -EINVAL;

        ret->length = epaddr.addr.size;
        memcpy(ret->bytes, epaddr.addr.data, epaddr.addr.size);
        return 0;
}

#define UPDATE(dest, val, updated)                     \
        do {                                           \
                typeof(val) _v = (val);                \
                if (dest != _v)                        \
                        updated = true;                \
                dest = _v;                             \
        } while (false)

#define UPDATE_WITH_MAX(dest, max, val, updated)       \
        do {                                           \
                typeof(dest) _v = (val);               \
                typeof(dest) _max = (max);             \
                if (_v == 0 || _v > _max)              \
                        _v = _max;                     \
                if (dest != _v)                        \
                        updated = true;                \
                dest = _v;                             \
        } while (false)

int ethtool_set_wol(
                int *ethtool_fd,
                const char *ifname,
                uint32_t wolopts,
                const uint8_t password[SOPASS_MAX]) {

        struct ethtool_wolinfo ecmd = {
                .cmd = ETHTOOL_GWOL,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        bool need_update = false;
        int r;

        assert(ethtool_fd);
        assert(ifname);

        if (wolopts == UINT32_MAX && !password)
                /* Nothing requested. Return earlier. */
                return 0;

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        CLEANUP_ERASE(ecmd);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (wolopts == UINT32_MAX) {
                /* When password is specified without valid WoL options specified, then enable
                 * WAKE_MAGICSECURE flag if supported. */
                wolopts = ecmd.wolopts;
                if (password && FLAGS_SET(ecmd.supported, WAKE_MAGICSECURE))
                        wolopts |= WAKE_MAGICSECURE;
        }

        if ((wolopts & ~ecmd.supported) != 0) {
                _cleanup_free_ char *str = NULL;

                (void) wol_options_to_string_alloc(wolopts & ~ecmd.supported, &str);
                log_debug("Network interface %s does not support requested Wake on LAN options \"%s\", ignoring.",
                          ifname, strna(str));

                wolopts &= ecmd.supported;
        }

        if (!FLAGS_SET(wolopts, WAKE_MAGICSECURE))
                /* When WAKE_MAGICSECURE flag is not set, then ignore password. */
                password = NULL;

        UPDATE(ecmd.wolopts, wolopts, need_update);
        if (password &&
            memcmp(ecmd.sopass, password, sizeof(ecmd.sopass)) != 0) {
                memcpy(ecmd.sopass, password, sizeof(ecmd.sopass));
                need_update = true;
        }

        if (!need_update)
                return 0;

        ecmd.cmd = ETHTOOL_SWOL;
        return RET_NERRNO(ioctl(*ethtool_fd, SIOCETHTOOL, &ifr));
}

int ethtool_set_nic_buffer_size(int *ethtool_fd, const char *ifname, const netdev_ring_param *ring) {
        struct ethtool_ringparam ecmd = {
                .cmd = ETHTOOL_GRINGPARAM,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        bool need_update = false;
        int r;

        assert(ethtool_fd);
        assert(ifname);
        assert(ring);

        if (!ring->rx.set &&
            !ring->rx_mini.set &&
            !ring->rx_jumbo.set &&
            !ring->tx.set)
                return 0;

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (ring->rx.set)
                UPDATE_WITH_MAX(ecmd.rx_pending, ecmd.rx_max_pending, ring->rx.value, need_update);

        if (ring->rx_mini.set)
                UPDATE_WITH_MAX(ecmd.rx_mini_pending, ecmd.rx_mini_max_pending, ring->rx_mini.value, need_update);

        if (ring->rx_jumbo.set)
                UPDATE_WITH_MAX(ecmd.rx_jumbo_pending, ecmd.rx_jumbo_max_pending, ring->rx_jumbo.value, need_update);

        if (ring->tx.set)
                UPDATE_WITH_MAX(ecmd.tx_pending, ecmd.tx_max_pending, ring->tx.value, need_update);

        if (!need_update)
                return 0;

        ecmd.cmd = ETHTOOL_SRINGPARAM;
        return RET_NERRNO(ioctl(*ethtool_fd, SIOCETHTOOL, &ifr));
}

static int get_stringset(int ethtool_fd, const char *ifname, enum ethtool_stringset stringset_id, struct ethtool_gstrings **ret) {
        _cleanup_free_ struct ethtool_gstrings *strings = NULL;
        uint8_t buffer[offsetof(struct ethtool_sset_info, data) + sizeof(uint32_t)] = {};
        struct ifreq ifr = {
                .ifr_data = (void*) buffer,
        };
        uint32_t len;

        assert(ethtool_fd >= 0);
        assert(ifname);
        assert(ret);

        struct ethtool_sset_info *info = (struct ethtool_sset_info*) buffer;
        info->cmd = ETHTOOL_GSSET_INFO;
        info->sset_mask = UINT64_C(1) << stringset_id;
        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (info->sset_mask == 0)
                return -EOPNOTSUPP;

        len = *info->data;
        if (len == 0)
                return -EOPNOTSUPP;

        strings = malloc0(offsetof(struct ethtool_gstrings, data) + len * ETH_GSTRING_LEN);
        if (!strings)
                return -ENOMEM;

        strings->cmd = ETHTOOL_GSTRINGS;
        strings->string_set = stringset_id;
        strings->len = len;

        ifr.ifr_data = (void*) strings;

        if (ioctl(ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        *ret = TAKE_PTR(strings);
        return 0;
}

static int get_features(int ethtool_fd, const char *ifname, uint32_t n_features, struct ethtool_gfeatures **ret) {
        _cleanup_free_ struct ethtool_gfeatures *gfeatures = NULL;
        struct ifreq ifr;

        assert(ethtool_fd >= 0);
        assert(ifname);
        assert(ret);
        assert(n_features > 0);

        gfeatures = malloc0(offsetof(struct ethtool_gfeatures, features) +
                            DIV_ROUND_UP(n_features, 32U) * sizeof(gfeatures->features[0]));
        if (!gfeatures)
                return -ENOMEM;

        gfeatures->cmd = ETHTOOL_GFEATURES;
        gfeatures->size = DIV_ROUND_UP(n_features, 32U);

        ifr = (struct ifreq) {
                .ifr_data = (void*) gfeatures,
        };
        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        *ret = TAKE_PTR(gfeatures);
        return 0;
}

static int set_features_bit(
                const struct ethtool_gstrings *strings,
                const struct ethtool_gfeatures *gfeatures,
                struct ethtool_sfeatures *sfeatures,
                const char *feature,
                int flag) {

        assert(strings);
        assert(gfeatures);
        assert(sfeatures);
        assert(feature);

        if (flag < 0)
                return 0;

        for (uint32_t i = 0; i < strings->len; i++) {
                uint32_t block, mask;

                if (!strneq((const char*) &strings->data[i * ETH_GSTRING_LEN], feature, ETH_GSTRING_LEN))
                        continue;

                block = i / 32;
                mask = UINT32_C(1) << (i % 32);

                if (!FLAGS_SET(gfeatures->features[block].available, mask) ||
                    FLAGS_SET(gfeatures->features[block].never_changed, mask))
                        return -EOPNOTSUPP;

                sfeatures->features[block].valid |= mask;
                SET_FLAG(sfeatures->features[block].requested, mask, flag);

                return 0;
        }

        return -ENODATA;
}

static int set_features_multiple_bit(
                const struct ethtool_gstrings *strings,
                const struct ethtool_gfeatures *gfeatures,
                struct ethtool_sfeatures *sfeatures,
                const char *feature,
                int flag) {

        bool found = false;
        int r = -ENODATA;

        assert(strings);
        assert(gfeatures);
        assert(sfeatures);
        assert(feature);

        if (flag < 0)
                return 0;

        for (uint32_t i = 0; i < strings->len; i++) {
                uint32_t block, mask;

                if (!startswith((const char*) &strings->data[i * ETH_GSTRING_LEN], feature))
                        continue;

                block = i / 32;
                mask = UINT32_C(1) << (i % 32);

                if (!FLAGS_SET(gfeatures->features[block].available, mask) ||
                    FLAGS_SET(gfeatures->features[block].never_changed, mask)) {
                        r = -EOPNOTSUPP;
                        continue;
                }

                /* The flags is explicitly set by set_features_bit() */
                if (FLAGS_SET(sfeatures->features[block].valid, mask))
                        continue;

                sfeatures->features[block].valid |= mask;
                SET_FLAG(sfeatures->features[block].requested, mask, flag);

                found = true;
        }

        return found ? 0 : r;
}

int ethtool_set_features(int *ethtool_fd, const char *ifname, const int features[static _NET_DEV_FEAT_MAX]) {
        _cleanup_free_ struct ethtool_gstrings *strings = NULL;
        _cleanup_free_ struct ethtool_gfeatures *gfeatures = NULL;
        _cleanup_free_ struct ethtool_sfeatures *sfeatures = NULL;
        struct ifreq ifr;
        bool have = false;
        int r;

        assert(ethtool_fd);
        assert(ifname);
        assert(features);

        for (size_t i = 0; i < _NET_DEV_FEAT_MAX; i++)
                if (features[i] >= 0) {
                        have = true;
                        break;
                }

        if (!have)
                return 0;

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        r = get_stringset(*ethtool_fd, ifname, ETH_SS_FEATURES, &strings);
        if (r < 0)
                return log_debug_errno(r, "ethtool: could not get ethtool feature strings: %m");

        r = get_features(*ethtool_fd, ifname, strings->len, &gfeatures);
        if (r < 0)
                return log_debug_errno(r, "ethtool: could not get ethtool features for %s: %m", ifname);

        sfeatures = malloc0(offsetof(struct ethtool_sfeatures, features) +
                            DIV_ROUND_UP(strings->len, 32U) * sizeof(sfeatures->features[0]));
        if (!sfeatures)
                return log_oom_debug();

        sfeatures->cmd = ETHTOOL_SFEATURES;
        sfeatures->size = DIV_ROUND_UP(strings->len, 32U);

        for (size_t i = 0; i < _NET_DEV_FEAT_SIMPLE_MAX; i++) {
                r = set_features_bit(strings, gfeatures, sfeatures, netdev_feature_table[i], features[i]);
                if (r < 0)
                        log_debug_errno(r, "ethtool: could not set feature %s for %s, ignoring: %m", netdev_feature_table[i], ifname);
        }

        for (size_t i = _NET_DEV_FEAT_SIMPLE_MAX; i < _NET_DEV_FEAT_MAX; i++) {
                r = set_features_multiple_bit(strings, gfeatures, sfeatures, netdev_feature_table[i], features[i]);
                if (r < 0)
                        log_debug_errno(r, "ethtool: could not set feature %s for %s, ignoring: %m", netdev_feature_table[i], ifname);
        }

        ifr = (struct ifreq) {
                .ifr_data = (void*) sfeatures,
        };
        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return log_debug_errno(errno, "ethtool: could not set ethtool features for %s", ifname);

        return 0;
}

static int get_link_settings(int fd, struct ifreq *ifr, struct ethtool_link_settings **ret) {
        struct ethtool_link_settings ecmd = {
                .cmd = ETHTOOL_GLINKSETTINGS,
        };

        assert(fd >= 0);
        assert(ifr);
        assert(ret);

        /* The interaction user/kernel via the new API requires a small ETHTOOL_GLINKSETTINGS handshake first
         * to agree on the length of the link mode bitmaps. If kernel doesn't agree with user, it returns the
         * bitmap length it is expecting from user as a negative length. When kernel and user agree, kernel
         * returns valid info in all fields (with bitmap length > 0).
         * https://github.com/torvalds/linux/commit/3f1ac7a700d039c61d8d8b99f28d605d489a60cf
         * https://github.com/torvalds/linux/commit/793cf87de9d1a62dc9079c3ec5fcc01cfc62fafb */

        ifr->ifr_data = &ecmd;

        if (ioctl(fd, SIOCETHTOOL, ifr) < 0)
                return -errno;

        if (ecmd.link_mode_masks_nwords >= 0 || ecmd.cmd != ETHTOOL_GLINKSETTINGS)
                return -EOPNOTSUPP;

        int8_t n = -ecmd.link_mode_masks_nwords;
        size_t sz = offsetof(struct ethtool_link_settings, link_mode_masks) + sizeof(uint32_t) * n * 3;
        _cleanup_free_ struct ethtool_link_settings *settings = calloc(sz, 1);
        if (!settings)
                return -ENOMEM;

        settings->cmd = ETHTOOL_GLINKSETTINGS;
        settings->link_mode_masks_nwords = n;

        ifr->ifr_data = settings;
        if (ioctl(fd, SIOCETHTOOL, ifr) < 0)
                return -errno;

        if (settings->link_mode_masks_nwords != n || settings->cmd != ETHTOOL_GLINKSETTINGS)
                return -EOPNOTSUPP;

        *ret = TAKE_PTR(settings);
        return 0;
}

int ethtool_set_link_settings(
                int *fd,
                const char *ifname,
                int autonegotiation,
                const uint32_t advertise[static N_ADVERTISE],
                uint64_t speed,
                Duplex duplex,
                NetDevPort port,
                uint8_t mdi) {

        _cleanup_free_ struct ethtool_link_settings *settings = NULL;
        struct ifreq ifr = {};
        bool changed = false;
        int r;

        assert(fd);
        assert(ifname);
        assert(advertise);

        if (autonegotiation < 0 && memeqzero(advertise, sizeof(uint32_t) * N_ADVERTISE) &&
            speed == 0 && duplex < 0 && port < 0 && mdi == ETH_TP_MDI_INVALID)
                return 0;

        /* If autonegotiation is disabled, the speed and duplex represent the fixed link mode and are
         * writable if the driver supports multiple link modes. If it is enabled then they are
         * read-only. If the link is up they represent the negotiated link mode; if the link is down,
         * the speed is 0, %SPEED_UNKNOWN or the highest enabled speed and @duplex is %DUPLEX_UNKNOWN
         * or the best enabled duplex mode. */

        if (speed > 0 || duplex >= 0 || port >= 0) {
                if (autonegotiation == AUTONEG_ENABLE || !memeqzero(advertise, sizeof(uint32_t) * N_ADVERTISE)) {
                        log_debug("ethtool: autonegotiation is enabled, ignoring speed, duplex, or port settings.");
                        speed = 0;
                        duplex = _DUP_INVALID;
                        port = _NET_DEV_PORT_INVALID;
                } else {
                        log_debug("ethtool: setting speed, duplex, or port, disabling autonegotiation.");
                        autonegotiation = AUTONEG_DISABLE;
                }
        }

        r = ethtool_connect(fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        r = get_link_settings(*fd, &ifr, &settings);
        if (r < 0)
                return log_debug_errno(r, "ethtool: Cannot get link settings for %s: %m", ifname);

        if (speed > 0)
                UPDATE(settings->speed, DIV_ROUND_UP(speed, 1000000), changed);

        if (duplex >= 0)
                UPDATE(settings->duplex, duplex, changed);

        if (port >= 0)
                UPDATE(settings->port, port, changed);

        if (autonegotiation >= 0)
                UPDATE(settings->autoneg, autonegotiation, changed);

        if (!memeqzero(advertise, sizeof(uint32_t) * N_ADVERTISE)) {
                UPDATE(settings->autoneg, AUTONEG_ENABLE, changed);

                uint32_t *a = settings->link_mode_masks + settings->link_mode_masks_nwords;
                size_t n = MIN(settings->link_mode_masks_nwords, N_ADVERTISE);

                changed = changed ||
                        memcmp(a, advertise, sizeof(uint32_t) * n) != 0 ||
                        !memeqzero(a + n, sizeof(uint32_t) * (settings->link_mode_masks_nwords - n));

                memcpy(a, advertise, sizeof(uint32_t) * n);
                memzero(a + n, sizeof(uint32_t) * (settings->link_mode_masks_nwords - n));
        }

        if (mdi != ETH_TP_MDI_INVALID) {
                if (settings->eth_tp_mdix_ctrl == ETH_TP_MDI_INVALID)
                        log_debug("ethtool: setting MDI not supported for %s, ignoring.", ifname);
                else
                        UPDATE(settings->eth_tp_mdix_ctrl, mdi, changed);
        }

        if (!changed)
                return 0;

        settings->cmd = ETHTOOL_SLINKSETTINGS;
        ifr.ifr_data = settings;
        if (ioctl(*fd, SIOCETHTOOL, &ifr) < 0)
                return log_debug_errno(errno, "ethtool: Cannot set link settings for %s: %m", ifname);

        return r;
}

int ethtool_set_channels(int *fd, const char *ifname, const netdev_channels *channels) {
        struct ethtool_channels ecmd = {
                .cmd = ETHTOOL_GCHANNELS,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        bool need_update = false;
        int r;

        assert(fd);
        assert(ifname);
        assert(channels);

        if (!channels->rx.set &&
            !channels->tx.set &&
            !channels->other.set &&
            !channels->combined.set)
                return 0;

        r = ethtool_connect(fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (channels->rx.set)
                UPDATE_WITH_MAX(ecmd.rx_count, ecmd.max_rx, channels->rx.value, need_update);

        if (channels->tx.set)
                UPDATE_WITH_MAX(ecmd.tx_count, ecmd.max_tx, channels->tx.value, need_update);

        if (channels->other.set)
                UPDATE_WITH_MAX(ecmd.other_count, ecmd.max_other, channels->other.value, need_update);

        if (channels->combined.set)
                UPDATE_WITH_MAX(ecmd.combined_count, ecmd.max_combined, channels->combined.value, need_update);

        if (!need_update)
                return 0;

        ecmd.cmd = ETHTOOL_SCHANNELS;
        return RET_NERRNO(ioctl(*fd, SIOCETHTOOL, &ifr));
}

int ethtool_set_flow_control(int *fd, const char *ifname, int rx, int tx, int autoneg) {
        struct ethtool_pauseparam ecmd = {
                .cmd = ETHTOOL_GPAUSEPARAM,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        bool need_update = false;
        int r;

        assert(fd);
        assert(ifname);

        if (rx < 0 && tx < 0 && autoneg < 0)
                return 0;

        r = ethtool_connect(fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (rx >= 0)
                UPDATE(ecmd.rx_pause, (uint32_t) rx, need_update);

        if (tx >= 0)
                UPDATE(ecmd.tx_pause, (uint32_t) tx, need_update);

        if (autoneg >= 0)
                UPDATE(ecmd.autoneg, (uint32_t) autoneg, need_update);

        if (!need_update)
                return 0;

        ecmd.cmd = ETHTOOL_SPAUSEPARAM;
        return RET_NERRNO(ioctl(*fd, SIOCETHTOOL, &ifr));
}

int ethtool_set_nic_coalesce_settings(int *ethtool_fd, const char *ifname, const netdev_coalesce_param *coalesce) {
        struct ethtool_coalesce ecmd = {
                .cmd = ETHTOOL_GCOALESCE,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };
        bool need_update = false;
        int r;

        assert(ethtool_fd);
        assert(ifname);
        assert(coalesce);

        if (coalesce->use_adaptive_rx_coalesce < 0 &&
            coalesce->use_adaptive_tx_coalesce < 0 &&
            !coalesce->rx_coalesce_usecs.set &&
            !coalesce->rx_max_coalesced_frames.set &&
            !coalesce->rx_coalesce_usecs_irq.set &&
            !coalesce->rx_max_coalesced_frames_irq.set &&
            !coalesce->tx_coalesce_usecs.set &&
            !coalesce->tx_max_coalesced_frames.set &&
            !coalesce->tx_coalesce_usecs_irq.set &&
            !coalesce->tx_max_coalesced_frames_irq.set &&
            !coalesce->stats_block_coalesce_usecs.set &&
            !coalesce->pkt_rate_low.set &&
            !coalesce->rx_coalesce_usecs_low.set &&
            !coalesce->rx_max_coalesced_frames_low.set &&
            !coalesce->tx_coalesce_usecs_low.set &&
            !coalesce->tx_max_coalesced_frames_low.set &&
            !coalesce->pkt_rate_high.set &&
            !coalesce->rx_coalesce_usecs_high.set &&
            !coalesce->rx_max_coalesced_frames_high.set &&
            !coalesce->tx_coalesce_usecs_high.set &&
            !coalesce->tx_max_coalesced_frames_high.set &&
            !coalesce->rate_sample_interval.set)
                return 0;

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (coalesce->use_adaptive_rx_coalesce >= 0)
                UPDATE(ecmd.use_adaptive_rx_coalesce, (uint32_t) coalesce->use_adaptive_rx_coalesce, need_update);

        if (coalesce->use_adaptive_tx_coalesce >= 0)
                UPDATE(ecmd.use_adaptive_tx_coalesce, (uint32_t) coalesce->use_adaptive_tx_coalesce, need_update);

        if (coalesce->rx_coalesce_usecs.set)
                UPDATE(ecmd.rx_coalesce_usecs, coalesce->rx_coalesce_usecs.value, need_update);

        if (coalesce->rx_max_coalesced_frames.set)
                UPDATE(ecmd.rx_max_coalesced_frames, coalesce->rx_max_coalesced_frames.value, need_update);

        if (coalesce->rx_coalesce_usecs_irq.set)
                UPDATE(ecmd.rx_coalesce_usecs_irq, coalesce->rx_coalesce_usecs_irq.value, need_update);

        if (coalesce->rx_max_coalesced_frames_irq.set)
                UPDATE(ecmd.rx_max_coalesced_frames_irq, coalesce->rx_max_coalesced_frames_irq.value, need_update);

        if (coalesce->tx_coalesce_usecs.set)
                UPDATE(ecmd.tx_coalesce_usecs, coalesce->tx_coalesce_usecs.value, need_update);

        if (coalesce->tx_max_coalesced_frames.set)
                UPDATE(ecmd.tx_max_coalesced_frames, coalesce->tx_max_coalesced_frames.value, need_update);

        if (coalesce->tx_coalesce_usecs_irq.set)
                UPDATE(ecmd.tx_coalesce_usecs_irq, coalesce->tx_coalesce_usecs_irq.value, need_update);

        if (coalesce->tx_max_coalesced_frames_irq.set)
                UPDATE(ecmd.tx_max_coalesced_frames_irq, coalesce->tx_max_coalesced_frames_irq.value, need_update);

        if (coalesce->stats_block_coalesce_usecs.set)
                UPDATE(ecmd.stats_block_coalesce_usecs, coalesce->stats_block_coalesce_usecs.value, need_update);

        if (coalesce->pkt_rate_low.set)
                UPDATE(ecmd.pkt_rate_low, coalesce->pkt_rate_low.value, need_update);

        if (coalesce->rx_coalesce_usecs_low.set)
                UPDATE(ecmd.rx_coalesce_usecs_low, coalesce->rx_coalesce_usecs_low.value, need_update);

        if (coalesce->rx_max_coalesced_frames_low.set)
                UPDATE(ecmd.rx_max_coalesced_frames_low, coalesce->rx_max_coalesced_frames_low.value, need_update);

        if (coalesce->tx_coalesce_usecs_low.set)
                UPDATE(ecmd.tx_coalesce_usecs_low, coalesce->tx_coalesce_usecs_low.value, need_update);

        if (coalesce->tx_max_coalesced_frames_low.set)
                UPDATE(ecmd.tx_max_coalesced_frames_low, coalesce->tx_max_coalesced_frames_low.value, need_update);

        if (coalesce->pkt_rate_high.set)
                UPDATE(ecmd.pkt_rate_high, coalesce->pkt_rate_high.value, need_update);

        if (coalesce->rx_coalesce_usecs_high.set)
                UPDATE(ecmd.rx_coalesce_usecs_high, coalesce->rx_coalesce_usecs_high.value, need_update);

        if (coalesce->rx_max_coalesced_frames_high.set)
                UPDATE(ecmd.rx_max_coalesced_frames_high, coalesce->rx_max_coalesced_frames_high.value, need_update);

        if (coalesce->tx_coalesce_usecs_high.set)
                UPDATE(ecmd.tx_coalesce_usecs_high, coalesce->tx_coalesce_usecs_high.value, need_update);

        if (coalesce->tx_max_coalesced_frames_high.set)
                UPDATE(ecmd.tx_max_coalesced_frames_high, coalesce->tx_max_coalesced_frames_high.value, need_update);

        if (coalesce->rate_sample_interval.set)
                UPDATE(ecmd.rate_sample_interval, DIV_ROUND_UP(coalesce->rate_sample_interval.value, USEC_PER_SEC), need_update);

        if (!need_update)
                return 0;

        ecmd.cmd = ETHTOOL_SCOALESCE;
        return RET_NERRNO(ioctl(*ethtool_fd, SIOCETHTOOL, &ifr));
}

int ethtool_set_eee_settings(
                int *ethtool_fd,
                const char *ifname,
                int enabled,
                int tx_lpi_enabled,
                usec_t tx_lpi_timer_usec,
                uint32_t advertise) {

        int r;

        assert(ethtool_fd);
        assert(ifname);

        if (enabled < 0 &&
            tx_lpi_enabled < 0 &&
            tx_lpi_timer_usec == USEC_INFINITY &&
            advertise == 0)
                return 0; /* Nothing requested. */

        r = ethtool_connect(ethtool_fd);
        if (r < 0)
                return r;

        struct ethtool_eee ecmd = {
                .cmd = ETHTOOL_GEEE,
        };
        struct ifreq ifr = {
                .ifr_data = (void*) &ecmd,
        };

        strscpy(ifr.ifr_name, sizeof(ifr.ifr_name), ifname);

        if (ioctl(*ethtool_fd, SIOCETHTOOL, &ifr) < 0)
                return -errno;

        if (ecmd.supported == 0)
                return 0; /* Unsupported. */

        bool need_update = false;

        if (enabled >= 0)
                UPDATE(ecmd.eee_enabled, (uint32_t) enabled, need_update);
        if (tx_lpi_enabled >= 0)
                UPDATE(ecmd.tx_lpi_enabled, (uint32_t) tx_lpi_enabled, need_update);
        if (tx_lpi_timer_usec != USEC_INFINITY)
                UPDATE(ecmd.tx_lpi_timer, (uint32_t) MIN(DIV_ROUND_UP(tx_lpi_timer_usec, USEC_PER_MSEC), (usec_t) UINT32_MAX), need_update);
        if (advertise != 0)
                UPDATE(ecmd.advertised, advertise & ecmd.supported, need_update);

        if (!need_update)
                return 0; /* Nothing changed. */

        ecmd.cmd = ETHTOOL_SEEE;
        return RET_NERRNO(ioctl(*ethtool_fd, SIOCETHTOOL, &ifr));
}

int config_parse_advertise(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        uint32_t *advertise = ASSERT_PTR(data);
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);

        if (isempty(rvalue)) {
                /* Empty string resets the value. */
                memzero(advertise, sizeof(uint32_t) * N_ADVERTISE);
                return 0;
        }

        for (const char *p = rvalue;;) {
                _cleanup_free_ char *w = NULL;
                enum ethtool_link_mode_bit_indices mode;

                r = extract_first_word(&p, &w, NULL, 0);
                if (r == -ENOMEM)
                        return log_oom();
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r,
                                   "Failed to split advertise modes '%s', ignoring assignment: %m", rvalue);
                        return 0;
                }
                if (r == 0)
                        return 0;

                mode = ethtool_link_mode_bit_from_string(w);
                /* We reuse the kernel provided enum which does not contain negative value. So, the cast
                 * below is mandatory. Otherwise, the check below always passes and access an invalid address. */
                if ((int) mode < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, mode,
                                   "Failed to parse advertise mode, ignoring: %s", w);
                        continue;
                }

                advertise[mode / 32] |= 1UL << (mode % 32);
        }
}

int config_parse_mdi(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        uint8_t *mdi = ASSERT_PTR(data);

        assert(filename);
        assert(rvalue);

        if (isempty(rvalue)) {
                *mdi = ETH_TP_MDI_INVALID;
                return 0;
        }

        if (STR_IN_SET(rvalue, "mdi", "straight")) {
                *mdi = ETH_TP_MDI;
                return 0;
        }

        if (STR_IN_SET(rvalue, "mdi-x", "mdix", "crossover")) {
                *mdi = ETH_TP_MDI_X;
                return 0;
        }

        if (streq(rvalue, "auto")) {
                *mdi = ETH_TP_MDI_AUTO;
                return 0;
        }

        log_syntax(unit, LOG_WARNING, filename, line, 0,
                   "Failed to parse %s= setting, ignoring assignment: %s", lvalue, rvalue);
        return 0;
}

int config_parse_ring_buffer_or_channel(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        u32_opt *dst = ASSERT_PTR(data);
        uint32_t k;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);

        if (isempty(rvalue)) {
                dst->value = 0;
                dst->set = false;
                return 0;
        }

        if (streq(rvalue, "max")) {
                dst->value = 0;
                dst->set = true;
                return 0;
        }

        r = safe_atou32(rvalue, &k);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse %s=, ignoring: %s", lvalue, rvalue);
                return 0;
        }
        if (k < 1) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Invalid %s= value, ignoring: %s", lvalue, rvalue);
                return 0;
        }

        dst->value = k;
        dst->set = true;
        return 0;
}

int config_parse_wol(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {

        uint32_t new_opts = 0, *opts = data;
        int r;

        assert(filename);
        assert(section);
        assert(lvalue);
        assert(rvalue);
        assert(data);

        if (isempty(rvalue)) {
                *opts = UINT32_MAX; /* Do not update WOL option. */
                return 0;
        }

        if (streq(rvalue, "off")) {
                *opts = 0; /* Disable WOL. */
                return 0;
        }

        for (const char *p = rvalue;;) {
                _cleanup_free_ char *w = NULL;
                bool found = false;

                r = extract_first_word(&p, &w, NULL, 0);
                if (r == -ENOMEM)
                        return log_oom();
                if (r < 0) {
                        log_syntax(unit, LOG_WARNING, filename, line, r,
                                   "Failed to split wake-on-lan modes '%s', ignoring assignment: %m", rvalue);
                        return 0;
                }
                if (r == 0)
                        break;

                FOREACH_ELEMENT(option, wol_option_map)
                        if (streq(w, option->name)) {
                                new_opts |= option->opt;
                                found = true;
                                break;
                        }

                if (!found)
                        log_syntax(unit, LOG_WARNING, filename, line, 0,
                                   "Unknown wake-on-lan mode '%s', ignoring.", w);
        }

        if (*opts == UINT32_MAX)
                *opts = new_opts;
        else
                *opts |= new_opts;

        return 0;
}

int config_parse_coalesce_u32(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {
        u32_opt *dst = data;
        uint32_t k;
        int r;

        if (isempty(rvalue)) {
                dst->value = 0;
                dst->set = false;
                return 0;
        }

        r = safe_atou32(rvalue, &k);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse %s=, ignoring: %s", lvalue, rvalue);
                return 0;
        }

        dst->value = k;
        dst->set = true;
        return 0;
}

int config_parse_coalesce_sec(
                const char *unit,
                const char *filename,
                unsigned line,
                const char *section,
                unsigned section_line,
                const char *lvalue,
                int ltype,
                const char *rvalue,
                void *data,
                void *userdata) {
        u32_opt *dst = data;
        usec_t usec;
        int r;

        if (isempty(rvalue)) {
                dst->value = 0;
                dst->set = false;
                return 0;
        }

        r = parse_sec(rvalue, &usec);
        if (r < 0) {
                log_syntax(unit, LOG_WARNING, filename, line, r,
                           "Failed to parse coalesce setting value, ignoring: %s", rvalue);
                return 0;
        }

        if (usec > UINT32_MAX) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Too large %s= value, ignoring: %s", lvalue, rvalue);
                return 0;
        }

        if (STR_IN_SET(lvalue, "StatisticsBlockCoalesceSec", "CoalescePacketRateSampleIntervalSec") && usec < 1) {
                log_syntax(unit, LOG_WARNING, filename, line, 0,
                           "Invalid %s= value, ignoring: %s", lvalue, rvalue);
                return 0;
        }

        dst->value = (uint32_t) usec;
        dst->set = true;

        return 0;
}
