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

#include "sd-netlink.h"

#include "ether-addr-util.h"
#include "fd-util.h"
#include "hashmap.h"
#include "iovec-util.h"
#include "log.h"
#include "memory-util.h"
#include "netlink-internal.h"
#include "netlink-util.h"
#include "parse-util.h"
#include "process-util.h"
#include "socket-util.h"
#include "string-util.h"
#include "strv.h"

static int parse_newlink_message(
                  sd_netlink_message *message,
                  char **ret_name,
                  char ***ret_altnames) {

        _cleanup_strv_free_ char **altnames = NULL;
        int r, ifindex;

        assert(message);

        uint16_t type;
        r = sd_netlink_message_get_type(message, &type);
        if (r < 0)
                return r;
        if (type != RTM_NEWLINK)
                return -EPROTO;

        r = sd_rtnl_message_link_get_ifindex(message, &ifindex);
        if (r < 0)
                return r;
        if (ifindex <= 0)
                return -EPROTO;

        if (ret_altnames) {
                r = sd_netlink_message_read_strv(message, IFLA_PROP_LIST, IFLA_ALT_IFNAME, &altnames);
                if (r < 0 && r != -ENODATA)
                        return r;
        }

        if (ret_name) {
                r = sd_netlink_message_read_string_strdup(message, IFLA_IFNAME, ret_name);
                if (r < 0)
                        return r;
        }

        if (ret_altnames)
                *ret_altnames = TAKE_PTR(altnames);

        return ifindex;
}

int rtnl_resolve_ifname_full(
                  sd_netlink **rtnl,
                  ResolveInterfaceNameFlag flags,
                  const char *name,
                  char **ret_name,
                  char ***ret_altnames) {

        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        int r;

        assert(name);
        assert(flags > 0);

        /* This is similar to if_nametoindex(), but also resolves alternative names and decimal formatted
         * ifindex too. Returns ifindex, and optionally provides the main interface name and alternative
         * names. */

        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        /* First, use IFLA_IFNAME */
        if (FLAGS_SET(flags, RESOLVE_IFNAME_MAIN) && ifname_valid(name)) {
                _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL, *reply = NULL;

                r = sd_rtnl_message_new_link(*rtnl, &message, RTM_GETLINK, 0);
                if (r < 0)
                        return r;

                r = sd_netlink_message_append_string(message, IFLA_IFNAME, name);
                if (r < 0)
                        return r;

                r = sd_netlink_call(*rtnl, message, 0, &reply);
                if (r >= 0)
                        return parse_newlink_message(reply, ret_name, ret_altnames);
                if (r != -ENODEV)
                        return r;
        }

        /* Next, try IFLA_ALT_IFNAME */
        if (FLAGS_SET(flags, RESOLVE_IFNAME_ALTERNATIVE) &&
            ifname_valid_full(name, IFNAME_VALID_ALTERNATIVE)) {
                _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL, *reply = NULL;

                r = sd_rtnl_message_new_link(*rtnl, &message, RTM_GETLINK, 0);
                if (r < 0)
                        return r;

                r = sd_netlink_message_append_string(message, IFLA_ALT_IFNAME, name);
                if (r < 0)
                        return r;

                r = sd_netlink_call(*rtnl, message, 0, &reply);
                if (r >= 0)
                        return parse_newlink_message(reply, ret_name, ret_altnames);
                if (r != -ENODEV)
                        return r;
        }

        /* Finally, assume the string is a decimal formatted ifindex. */
        if (FLAGS_SET(flags, RESOLVE_IFNAME_NUMERIC)) {
                int ifindex;

                ifindex = parse_ifindex(name);
                if (ifindex <= 0)
                        return -ENODEV;

                return rtnl_get_ifname_full(rtnl, ifindex, ret_name, ret_altnames);
        }

        return -ENODEV;
}

int rtnl_resolve_interface_or_warn(sd_netlink **rtnl, const char *name) {
        int r;

        assert(name);

        r = rtnl_resolve_interface(rtnl, name);
        if (r < 0)
                return log_error_errno(r, "Failed to resolve interface \"%s\": %m", name);
        return r;
}

static int set_link_name(sd_netlink *rtnl, int ifindex, const char *name) {
        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
        int r;

        assert(rtnl);
        assert(ifindex > 0);
        assert(name);

        /* Assign the requested name. */

        r = sd_rtnl_message_new_link(rtnl, &message, RTM_SETLINK, ifindex);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_string(message, IFLA_IFNAME, name);
        if (r < 0)
                return r;

        return sd_netlink_call(rtnl, message, 0, NULL);
}

int rtnl_rename_link(sd_netlink **rtnl, const char *orig_name, const char *new_name) {
        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        int r, ifindex;

        assert(orig_name);
        assert(new_name);

        /* This does not check alternative names. Callers must check the requested name is not used as an
         * alternative name. */

        if (streq(orig_name, new_name))
                return 0;

        if (!ifname_valid(new_name))
                return -EINVAL;

        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        ifindex = rtnl_resolve_ifname(rtnl, orig_name);
        if (ifindex < 0)
                return ifindex;

        return set_link_name(*rtnl, ifindex, new_name);
}

int rtnl_set_link_name(sd_netlink **rtnl, int ifindex, const char *name, char* const *alternative_names) {
        _cleanup_strv_free_ char **original_altnames = NULL, **new_altnames = NULL;
        bool altname_deleted = false;
        int r;

        assert(ifindex > 0);

        if (isempty(name) && strv_isempty(alternative_names))
                return 0;

        if (name && !ifname_valid(name))
                return -EINVAL;

        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        if (!rtnl)
                rtnl = &our_rtnl;

        /* If the requested name is already assigned as an alternative name, then first drop it. */
        r = rtnl_get_link_alternative_names(rtnl, ifindex, &original_altnames);
        if (r < 0)
                log_debug_errno(r, "Failed to get alternative names on network interface %i, ignoring: %m",
                                ifindex);

        if (name) {
                if (strv_contains(original_altnames, name)) {
                        r = rtnl_delete_link_alternative_names(rtnl, ifindex, STRV_MAKE(name));
                        if (r < 0)
                                return log_debug_errno(r, "Failed to remove '%s' from alternative names on network interface %i: %m",
                                                       name, ifindex);

                        altname_deleted = true;
                }

                r = set_link_name(*rtnl, ifindex, name);
                if (r < 0)
                        goto fail;
        }

        /* Filter out already assigned names from requested alternative names. Also, dedup the request. */
        STRV_FOREACH(a, alternative_names) {
                if (streq_ptr(name, *a))
                        continue;

                if (strv_contains(original_altnames, *a))
                        continue;

                if (strv_contains(new_altnames, *a))
                        continue;

                if (!ifname_valid_full(*a, IFNAME_VALID_ALTERNATIVE))
                        continue;

                r = strv_extend(&new_altnames, *a);
                if (r < 0)
                        return r;
        }

        strv_sort(new_altnames);

        /* Finally, assign alternative names. */
        r = rtnl_set_link_alternative_names(rtnl, ifindex, new_altnames);
        if (r == -EEXIST) /* Already assigned to another interface? */
                STRV_FOREACH(a, new_altnames) {
                        r = rtnl_set_link_alternative_names(rtnl, ifindex, STRV_MAKE(*a));
                        if (r < 0)
                                log_debug_errno(r, "Failed to assign '%s' as an alternative name on network interface %i, ignoring: %m",
                                                *a, ifindex);
                }
        else if (r < 0)
                log_debug_errno(r, "Failed to assign alternative names on network interface %i, ignoring: %m", ifindex);

        return 0;

fail:
        if (altname_deleted) {
                int q = rtnl_set_link_alternative_names(rtnl, ifindex, STRV_MAKE(name));
                if (q < 0)
                        log_debug_errno(q, "Failed to restore '%s' as an alternative name on network interface %i, ignoring: %m",
                                        name, ifindex);
        }

        return r;
}

int rtnl_set_link_properties(
                sd_netlink **rtnl,
                int ifindex,
                const char *alias,
                const struct hw_addr_data *hw_addr,
                uint32_t txqueues,
                uint32_t rxqueues,
                uint32_t txqueuelen,
                uint32_t mtu,
                uint32_t gso_max_size,
                size_t gso_max_segments) {

        int r;

        assert(ifindex > 0);

        if (!alias &&
            (!hw_addr || hw_addr->length == 0) &&
            txqueues == 0 &&
            rxqueues == 0 &&
            txqueuelen == UINT32_MAX &&
            mtu == 0 &&
            gso_max_size == 0 &&
            gso_max_segments == 0)
                return 0;

        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
        r = sd_rtnl_message_new_link(*rtnl, &message, RTM_SETLINK, ifindex);
        if (r < 0)
                return r;

        if (alias) {
                r = sd_netlink_message_append_string(message, IFLA_IFALIAS, alias);
                if (r < 0)
                        return r;
        }

        if (hw_addr && hw_addr->length > 0) {
                r = netlink_message_append_hw_addr(message, IFLA_ADDRESS, hw_addr);
                if (r < 0)
                        return r;
        }

        if (txqueues > 0) {
                r = sd_netlink_message_append_u32(message, IFLA_NUM_TX_QUEUES, txqueues);
                if (r < 0)
                        return r;
        }

        if (rxqueues > 0) {
                r = sd_netlink_message_append_u32(message, IFLA_NUM_RX_QUEUES, rxqueues);
                if (r < 0)
                        return r;
        }

        if (txqueuelen < UINT32_MAX) {
                r = sd_netlink_message_append_u32(message, IFLA_TXQLEN, txqueuelen);
                if (r < 0)
                        return r;
        }

        if (mtu != 0) {
                r = sd_netlink_message_append_u32(message, IFLA_MTU, mtu);
                if (r < 0)
                        return r;
        }

        if (gso_max_size > 0) {
                r = sd_netlink_message_append_u32(message, IFLA_GSO_MAX_SIZE, gso_max_size);
                if (r < 0)
                        return r;
        }

        if (gso_max_segments > 0) {
                r = sd_netlink_message_append_u32(message, IFLA_GSO_MAX_SEGS, gso_max_segments);
                if (r < 0)
                        return r;
        }

        r = sd_netlink_call(*rtnl, message, 0, NULL);
        if (r < 0)
                return r;

        return 0;
}

static int rtnl_update_link_alternative_names(
                sd_netlink **rtnl,
                uint16_t nlmsg_type,
                int ifindex,
                char* const *alternative_names) {

        int r;

        assert(ifindex > 0);
        assert(IN_SET(nlmsg_type, RTM_NEWLINKPROP, RTM_DELLINKPROP));

        if (strv_isempty(alternative_names))
                return 0;

        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
        r = sd_rtnl_message_new_link(*rtnl, &message, nlmsg_type, ifindex);
        if (r < 0)
                return r;

        r = sd_netlink_message_open_container(message, IFLA_PROP_LIST);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_strv(message, IFLA_ALT_IFNAME, (const char**) alternative_names);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(message);
        if (r < 0)
                return r;

        r = sd_netlink_call(*rtnl, message, 0, NULL);
        if (r < 0)
                return r;

        return 0;
}

int rtnl_set_link_alternative_names(sd_netlink **rtnl, int ifindex, char* const *alternative_names) {
        return rtnl_update_link_alternative_names(rtnl, RTM_NEWLINKPROP, ifindex, alternative_names);
}

int rtnl_delete_link_alternative_names(sd_netlink **rtnl, int ifindex, char* const *alternative_names) {
        return rtnl_update_link_alternative_names(rtnl, RTM_DELLINKPROP, ifindex, alternative_names);
}

int rtnl_set_link_alternative_names_by_ifname(
                sd_netlink **rtnl,
                const char *ifname,
                char* const *alternative_names) {

        int r;

        assert(rtnl);
        assert(ifname);

        if (strv_isempty(alternative_names))
                return 0;

        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL;
        r = sd_rtnl_message_new_link(*rtnl, &message, RTM_NEWLINKPROP, 0);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_string(message, IFLA_IFNAME, ifname);
        if (r < 0)
                return r;

        r = sd_netlink_message_open_container(message, IFLA_PROP_LIST);
        if (r < 0)
                return r;

        r = sd_netlink_message_append_strv(message, IFLA_ALT_IFNAME, (const char**) alternative_names);
        if (r < 0)
                return r;

        r = sd_netlink_message_close_container(message);
        if (r < 0)
                return r;

        r = sd_netlink_call(*rtnl, message, 0, NULL);
        if (r < 0)
                return r;

        return 0;
}

int rtnl_get_link_info_full(
                sd_netlink **rtnl,
                int ifindex,
                char **ret_name,
                char ***ret_altnames,
                unsigned short *ret_iftype,
                unsigned *ret_flags,
                char **ret_kind,
                struct hw_addr_data *ret_hw_addr,
                struct hw_addr_data *ret_permanent_hw_addr) {

        _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *message = NULL, *reply = NULL;
        _cleanup_(sd_netlink_unrefp) sd_netlink *our_rtnl = NULL;
        struct hw_addr_data addr = HW_ADDR_NULL, perm_addr = HW_ADDR_NULL;
        _cleanup_free_ char *name = NULL, *kind = NULL;
        _cleanup_strv_free_ char **altnames = NULL;
        unsigned short iftype;
        unsigned flags;
        int r;

        assert(ifindex > 0);

        if (!rtnl)
                rtnl = &our_rtnl;
        if (!*rtnl) {
                r = sd_netlink_open(rtnl);
                if (r < 0)
                        return r;
        }

        r = sd_rtnl_message_new_link(*rtnl, &message, RTM_GETLINK, ifindex);
        if (r < 0)
                return r;

        r = sd_netlink_call(*rtnl, message, 0, &reply);
        if (r == -EINVAL)
                return -ENODEV; /* The device does not exist */
        if (r < 0)
                return r;

        r = parse_newlink_message(reply, ret_name ? &name : NULL, ret_altnames ? &altnames : NULL);
        if (r < 0)
                return r;
        if (r != ifindex)
                return -EIO;

        if (ret_iftype) {
                r = sd_rtnl_message_link_get_type(reply, &iftype);
                if (r < 0)
                        return r;
        }

        if (ret_flags) {
                r = sd_rtnl_message_link_get_flags(reply, &flags);
                if (r < 0)
                        return r;
        }

        if (ret_kind) {
                r = sd_netlink_message_enter_container(reply, IFLA_LINKINFO);
                if (r >= 0) {
                        r = sd_netlink_message_read_string_strdup(reply, IFLA_INFO_KIND, &kind);
                        if (r < 0 && r != -ENODATA)
                                return r;

                        r = sd_netlink_message_exit_container(reply);
                        if (r < 0)
                                return r;
                }
        }

        if (ret_hw_addr) {
                r = netlink_message_read_hw_addr(reply, IFLA_ADDRESS, &addr);
                if (r < 0 && r != -ENODATA)
                        return r;
        }

        if (ret_permanent_hw_addr) {
                r = netlink_message_read_hw_addr(reply, IFLA_PERM_ADDRESS, &perm_addr);
                if (r < 0 && r != -ENODATA)
                        return r;
        }

        if (ret_name)
                *ret_name = TAKE_PTR(name);
        if (ret_altnames)
                *ret_altnames = TAKE_PTR(altnames);
        if (ret_iftype)
                *ret_iftype = iftype;
        if (ret_flags)
                *ret_flags = flags;
        if (ret_kind)
                *ret_kind = TAKE_PTR(kind);
        if (ret_hw_addr)
                *ret_hw_addr = addr;
        if (ret_permanent_hw_addr)
                *ret_permanent_hw_addr = perm_addr;
        return ifindex;
}

int rtnl_log_parse_error(int r) {
        return log_error_errno(r, "Failed to parse netlink message: %m");
}

int rtnl_log_create_error(int r) {
        return log_error_errno(r, "Failed to create netlink message: %m");
}

void rtattr_append_attribute_internal(struct rtattr *rta, unsigned short type, const void *data, size_t data_length) {
        size_t padding_length;
        uint8_t *padding;

        assert(rta);
        assert(!data || data_length > 0);

        /* fill in the attribute */
        rta->rta_type = type;
        rta->rta_len = RTA_LENGTH(data_length);
        if (data)
                /* we don't deal with the case where the user lies about the type
                 * and gives us too little data (so don't do that)
                 */
                padding = mempcpy(RTA_DATA(rta), data, data_length);

        else
                /* if no data was passed, make sure we still initialize the padding
                   note that we can have data_length > 0 (used by some containers) */
                padding = RTA_DATA(rta);

        /* make sure also the padding at the end of the message is initialized */
        padding_length = (uint8_t *) rta + RTA_SPACE(data_length) - padding;
        memzero(padding, padding_length);
}

int rtattr_append_attribute(struct rtattr **rta, unsigned short type, const void *data, size_t data_length) {
        struct rtattr *new_rta, *sub_rta;
        size_t message_length;

        assert(rta);
        assert(!data || data_length > 0);

        /* get the new message size (with padding at the end) */
        message_length = RTA_ALIGN(rta ? (*rta)->rta_len : 0) + RTA_SPACE(data_length);

        /* buffer should be smaller than both one page or 8K to be accepted by the kernel */
        if (message_length > MIN(page_size(), 8192UL))
                return -ENOBUFS;

        /* realloc to fit the new attribute */
        new_rta = realloc(*rta, message_length);
        if (!new_rta)
                return -ENOMEM;
        *rta = new_rta;

        /* get pointer to the attribute we are about to add */
        sub_rta = (struct rtattr *) ((uint8_t *) *rta + RTA_ALIGN((*rta)->rta_len));

        rtattr_append_attribute_internal(sub_rta, type, data, data_length);

        /* update rta_len */
        (*rta)->rta_len = message_length;

        return 0;
}

bool netlink_pid_changed(sd_netlink *nl) {
        /* We don't support people creating an nl connection and
         * keeping it around over a fork(). Let's complain. */
        return ASSERT_PTR(nl)->original_pid != getpid_cached();
}

static int socket_open(int family) {
        int fd;

        fd = socket(AF_NETLINK, SOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCK, family);
        if (fd < 0)
                return -errno;

        return fd_move_above_stdio(fd);
}

int netlink_open_family(sd_netlink **ret, int family) {
        _cleanup_close_ int fd = -EBADF;
        int r;

        fd = socket_open(family);
        if (fd < 0)
                return fd;

        r = sd_netlink_open_fd(ret, fd);
        if (r < 0)
                return r;
        TAKE_FD(fd);

        return 0;
}

static bool serial_used(sd_netlink *nl, uint32_t serial) {
        assert(nl);

        return
                hashmap_contains(nl->reply_callbacks, UINT32_TO_PTR(serial)) ||
                hashmap_contains(nl->rqueue_by_serial, UINT32_TO_PTR(serial)) ||
                hashmap_contains(nl->rqueue_partial_by_serial, UINT32_TO_PTR(serial));
}

void netlink_seal_message(sd_netlink *nl, sd_netlink_message *m) {
        uint32_t picked;

        assert(nl);
        assert(!netlink_pid_changed(nl));
        assert(m);
        assert(m->hdr);

        /* Avoid collisions with outstanding requests */
        do {
                picked = nl->serial;

                /* Don't use seq == 0, as that is used for broadcasts, so we would get confused by replies to
                   such messages */
                nl->serial = nl->serial == UINT32_MAX ? 1 : nl->serial + 1;

        } while (serial_used(nl, picked));

        m->hdr->nlmsg_seq = picked;
        message_seal(m);
}

static int socket_writev_message(sd_netlink *nl, sd_netlink_message **m, size_t msgcount) {
        _cleanup_free_ struct iovec *iovs = NULL;
        ssize_t k;

        assert(nl);
        assert(m);
        assert(msgcount > 0);

        iovs = new(struct iovec, msgcount);
        if (!iovs)
                return -ENOMEM;

        for (size_t i = 0; i < msgcount; i++) {
                assert(m[i]->hdr);
                assert(m[i]->hdr->nlmsg_len > 0);

                iovs[i] = IOVEC_MAKE(m[i]->hdr, m[i]->hdr->nlmsg_len);
        }

        k = writev(nl->fd, iovs, msgcount);
        if (k < 0)
                return -errno;

        return k;
}

int sd_netlink_sendv(
                sd_netlink *nl,
                sd_netlink_message **messages,
                size_t msgcount,
                uint32_t **ret_serial) {

        _cleanup_free_ uint32_t *serials = NULL;
        int r;

        assert_return(nl, -EINVAL);
        assert_return(!netlink_pid_changed(nl), -ECHILD);
        assert_return(messages, -EINVAL);
        assert_return(msgcount > 0, -EINVAL);

        if (ret_serial) {
                serials = new(uint32_t, msgcount);
                if (!serials)
                        return -ENOMEM;
        }

        for (size_t i = 0; i < msgcount; i++) {
                assert_return(!messages[i]->sealed, -EPERM);

                netlink_seal_message(nl, messages[i]);
                if (serials)
                        serials[i] = message_get_serial(messages[i]);
        }

        r = socket_writev_message(nl, messages, msgcount);
        if (r < 0)
                return r;

        if (ret_serial)
                *ret_serial = TAKE_PTR(serials);

        return r;
}
