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

#include <unistd.h>

#include "sd-daemon.h"
#include "sd-device.h"
#include "sd-netlink.h"
#include "sd-network.h"

#include "alloc-util.h"
#include "conf-files.h"
#include "edit-util.h"
#include "errno-util.h"
#include "extract-word.h"
#include "fd-util.h"
#include "log.h"
#include "mkdir.h"
#include "netlink-util.h"
#include "network-util.h"
#include "networkctl.h"
#include "networkctl-config-file.h"
#include "networkctl-util.h"
#include "pager.h"
#include "path-util.h"
#include "pretty-print.h"
#include "selinux-util.h"
#include "socket-util.h"
#include "stat-util.h"
#include "string-table.h"
#include "string-util.h"
#include "strv.h"
#include "terminal-util.h"
#include "virt.h"

typedef enum ReloadFlags {
        RELOAD_NETWORKD = 1 << 0,
        RELOAD_UDEVD    = 1 << 1,
} ReloadFlags;

typedef enum LinkConfigType {
        CONFIG_NETWORK,
        CONFIG_LINK,
        CONFIG_NETDEV,
        _CONFIG_MAX,
        _CONFIG_INVALID = -EINVAL,
} LinkConfigType;

static const char* const link_config_type_table[_CONFIG_MAX] = {
        [CONFIG_NETWORK] = "network",
        [CONFIG_LINK]    = "link",
        [CONFIG_NETDEV]  = "netdev",
};

DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(link_config_type, LinkConfigType);

static int get_config_files_by_name(
                const char *name,
                bool allow_masked,
                char **ret_path,
                char ***ret_dropins) {

        _cleanup_free_ char *path = NULL;
        int r;

        assert(filename_is_valid(name));
        assert(ret_path);

        STRV_FOREACH(i, NETWORK_DIRS) {
                _cleanup_free_ char *p = NULL;

                p = path_join(*i, name);
                if (!p)
                        return -ENOMEM;

                r = RET_NERRNO(access(p, F_OK));
                if (r >= 0) {
                        if (!allow_masked) {
                                r = null_or_empty_path(p);
                                if (r < 0)
                                        return log_debug_errno(r,
                                                               "Failed to check if network config '%s' is masked: %m",
                                                               name);
                                if (r > 0)
                                        return -ERFKILL;
                        }

                        path = TAKE_PTR(p);
                        break;
                }

                if (r != -ENOENT)
                        log_debug_errno(r, "Failed to determine whether '%s' exists, ignoring: %m", p);
        }

        if (!path)
                return -ENOENT;

        if (ret_dropins) {
                _cleanup_free_ char *dropin_dirname = NULL;

                dropin_dirname = strjoin(name, ".d");
                if (!dropin_dirname)
                        return -ENOMEM;

                r = conf_files_list_dropins(
                                ret_dropins,
                                dropin_dirname,
                                /* root= */ NULL,
                                /* root_fd= */ XAT_FDROOT,
                                CONF_FILES_WARN,
                                NETWORK_DIRS);
                if (r < 0)
                        return r;
        }

        *ret_path = TAKE_PTR(path);

        return 0;
}

static int get_dropin_by_name(
                const char *name,
                char * const *dropins,
                char **ret) {

        assert(name);
        assert(ret);

        STRV_FOREACH(i, dropins)
                if (path_equal_filename(*i, name)) {
                        _cleanup_free_ char *d = NULL;

                        d = strdup(*i);
                        if (!d)
                                return -ENOMEM;

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

        *ret = NULL;
        return 0;
}

static int get_network_files_by_link(
                const char *link,
                int ifindex,
                bool ignore_missing,
                char **ret_path,
                char ***ret_dropins) {

        _cleanup_strv_free_ char **dropins = NULL;
        _cleanup_free_ char *path = NULL;
        int r;

        assert(link);
        assert(ifindex > 0);
        assert(ret_path);
        assert(ret_dropins);

        r = sd_network_link_get_network_file(ifindex, &path);
        if (r == -ENODATA)
                return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ENOENT),
                                      "Link '%s' has no associated network file.", link);
        if (r < 0)
                return log_error_errno(r, "Failed to get network file for link '%s': %m", link);

        r = sd_network_link_get_network_file_dropins(ifindex, &dropins);
        if (r < 0 && r != -ENODATA)
                return log_error_errno(r, "Failed to get network drop-ins for link '%s': %m", link);

        *ret_path = TAKE_PTR(path);
        *ret_dropins = TAKE_PTR(dropins);

        return 0;
}

static int get_netdev_files_by_link(
                const char *link,
                int ifindex,
                bool ignore_missing,
                char **ret_path,
                char ***ret_dropins) {

        _cleanup_strv_free_ char **dropins = NULL;
        _cleanup_free_ char *path = NULL;
        int r;

        assert(link);
        assert(ifindex > 0);
        assert(ret_path);
        assert(ret_dropins);

        r = sd_network_link_get_netdev_file(ifindex, &path);
        if (r == -ENODATA)
                return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ENOENT),
                                      "Link '%s' has no associated netdev file.", link);
        if (r < 0)
                return log_error_errno(r, "Failed to get netdev file for link '%s': %m", link);

        r = sd_network_link_get_netdev_file_dropins(ifindex, &dropins);
        if (r < 0 && r != -ENODATA)
                return log_error_errno(r, "Failed to get netdev drop-ins for link '%s': %m", link);

        *ret_path = TAKE_PTR(path);
        *ret_dropins = TAKE_PTR(dropins);

        return 0;
}

static int get_link_files_by_link(const char *link, bool ignore_missing, char **ret_path, char ***ret_dropins) {
        _cleanup_(sd_device_unrefp) sd_device *device = NULL;
        _cleanup_strv_free_ char **dropins_split = NULL;
        _cleanup_free_ char *p = NULL;
        const char *path, *dropins;
        int r;

        assert(link);
        assert(ret_path);
        assert(ret_dropins);

        r = sd_device_new_from_ifname(&device, link);
        if (r < 0)
                return log_error_errno(r, "Failed to create sd-device object for link '%s': %m", link);

        r = sd_device_get_property_value(device, "ID_NET_LINK_FILE", &path);
        if (r == -ENOENT)
                return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, r,
                                      "Link '%s' has no associated link file.", link);
        if (r < 0)
                return log_error_errno(r, "Failed to get link file for link '%s': %m", link);

        r = sd_device_get_property_value(device, "ID_NET_LINK_FILE_DROPINS", &dropins);
        if (r < 0 && r != -ENOENT)
                return log_error_errno(r, "Failed to get link drop-ins for link '%s': %m", link);
        if (r >= 0) {
                r = strv_split_full(&dropins_split, dropins, ":", EXTRACT_CUNESCAPE);
                if (r < 0)
                        return log_error_errno(r, "Failed to parse link drop-ins for link '%s': %m", link);
        }

        p = strdup(path);
        if (!p)
                return log_oom();

        *ret_path = TAKE_PTR(p);
        *ret_dropins = TAKE_PTR(dropins_split);

        return 0;
}

static int get_config_files_by_link_config(
                const char *ifname,
                LinkConfigType type,
                bool ignore_missing,
                sd_netlink **rtnl,
                char **ret_path,
                char ***ret_dropins) {

        int r;

        assert(ifname);
        assert(type >= 0 && type < _CONFIG_MAX);
        assert(rtnl);
        assert(ret_path);
        assert(ret_dropins);

        if (type == CONFIG_LINK)
                return get_link_files_by_link(ifname, ignore_missing, ret_path, ret_dropins);

        if (!networkd_is_running())
                return log_full_errno(ignore_missing ? LOG_DEBUG : LOG_ERR, SYNTHETIC_ERRNO(ESRCH),
                                      "Cannot get network/netdev file for link if systemd-networkd is not running.");

        int ifindex = rtnl_resolve_interface_or_warn(rtnl, ifname);
        if (ifindex < 0)
                return ifindex;

        if (type == CONFIG_NETWORK)
                r = get_network_files_by_link(ifname, ifindex, ignore_missing, ret_path, ret_dropins);
        else if (type == CONFIG_NETDEV)
                r = get_netdev_files_by_link(ifname, ifindex, ignore_missing, ret_path, ret_dropins);
        else
                assert_not_reached();

        return r;
}

static int parse_link_config(const char *link_config, char **ret_ifname, LinkConfigType *ret_type) {
        const char *p = ASSERT_PTR(link_config);
        _cleanup_free_ char *ifname = NULL;
        int r;

        assert(ret_ifname);
        assert(ret_type);

        r = extract_first_word(&p, &ifname, ":", EXTRACT_DONT_COALESCE_SEPARATORS);
        if (r <= 0)
                return log_error_errno(r < 0 ? r : SYNTHETIC_ERRNO(EINVAL),
                                       "Failed to extract link name from '%s': %m", link_config);

        if (!ifname_valid_full(ifname, IFNAME_VALID_ALTERNATIVE | IFNAME_VALID_NUMERIC))
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid link name: %s", ifname);

        LinkConfigType t;

        if (isempty(p))
                t = CONFIG_NETWORK;
        else if (streq(p, "all"))
                t = _CONFIG_MAX;
        else {
                t = link_config_type_from_string(p);
                if (t < 0)
                        return log_error_errno(t, "Invalid config type '%s' for link '%s'.", p, ifname);
        }

        *ret_ifname = TAKE_PTR(ifname);
        *ret_type = t;

        return 0;
}

static int add_config_to_edit(
                EditFileContext *context,
                const char *path,
                char * const *dropins) {

        _cleanup_free_ char *new_path = NULL, *dropin_path = NULL, *old_dropin = NULL;
        _cleanup_strv_free_ char **comment_paths = NULL;
        int r;

        assert(context);
        assert(path);

        /* If we're supposed to edit main config file in /run/, but a config with the same name is present
         * under /etc/, we bail out since the one in /etc/ always overrides that in /run/. */
        if (arg_runtime && !arg_drop_in && path_startswith(path, "/etc"))
                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
                                       "Cannot edit runtime config file: overridden by %s", path);

        if (path_startswith(path, "/usr") || arg_runtime != !!path_startswith(path, "/run")) {
                _cleanup_free_ char *name = NULL;

                r = path_extract_filename(path, &name);
                if (r < 0)
                        return log_error_errno(r, "Failed to extract filename from '%s': %m", path);

                new_path = path_join(NETWORK_DIRS[arg_runtime ? 1 : 0], name);
                if (!new_path)
                        return log_oom();
        }

        if (!arg_drop_in)
                return edit_files_add(context, new_path ?: path, path, NULL);

        bool need_new_dropin;

        r = get_dropin_by_name(arg_drop_in, dropins, &old_dropin);
        if (r < 0)
                return log_error_errno(r, "Failed to acquire drop-in '%s': %m", arg_drop_in);
        if (r > 0) {
                /* See the explanation above */
                if (arg_runtime && path_startswith(old_dropin, "/etc"))
                        return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
                                               "Cannot edit runtime config file: overridden by %s", old_dropin);

                need_new_dropin = path_startswith(old_dropin, "/usr") || arg_runtime != !!path_startswith(old_dropin, "/run");
        } else
                need_new_dropin = true;

        if (!need_new_dropin)
                /* An existing drop-in is found in the correct scope. Let's edit it directly. */
                dropin_path = TAKE_PTR(old_dropin);
        else {
                /* No drop-in was found or an existing drop-in is in a different scope. Let's create a new
                 * drop-in file. */
                dropin_path = strjoin(new_path ?: path, ".d/", arg_drop_in);
                if (!dropin_path)
                        return log_oom();
        }

        comment_paths = strv_new(path);
        if (!comment_paths)
                return log_oom();

        r = strv_extend_strv(&comment_paths, dropins, /* filter_duplicates= */ false);
        if (r < 0)
                return log_oom();

        return edit_files_add(context, dropin_path, old_dropin, comment_paths);
}

static int reload_daemons(ReloadFlags flags) {
        int ret = 1;

        if (arg_no_reload)
                return 0;

        if (flags == 0)
                return 0;

        if (!sd_booted() || running_in_chroot() > 0) {
                log_debug("System is not booted with systemd or is running in chroot, skipping reload.");
                return 0;
        }

        if (FLAGS_SET(flags, RELOAD_UDEVD))
                RET_GATHER(ret, reload_udevd());

        if (FLAGS_SET(flags, RELOAD_NETWORKD)) {
                if (!networkd_is_running())
                        log_debug("systemd-networkd is not running, skipping reload.");
                else
                        RET_GATHER(ret, reload_networkd());
        }

        return ret;
}

int verb_edit(int argc, char *argv[], uintptr_t _data, void *userdata) {
        char **args = ASSERT_PTR(strv_skip(argv, 1));
        _cleanup_(edit_file_context_done) EditFileContext context = {
                .marker_start = DROPIN_MARKER_START,
                .marker_end = DROPIN_MARKER_END,
                .remove_parent = !!arg_drop_in,
                .read_from_stdin = arg_stdin,
        };
        _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
        ReloadFlags reload = 0;
        int r;

        if (!on_tty() && !arg_stdin)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Cannot edit network config files interactively if not on a tty.");

        /* Duplicating main configs makes no sense. This also mimics the behavior of systemctl. */
        if (arg_stdin && !arg_drop_in && strv_length(args) != 1)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "When 'edit --stdin' without '--drop-in=', exactly one config file for editing must be specified.");

        r = mac_selinux_init();
        if (r < 0)
                return r;

        STRV_FOREACH(name, args) {
                _cleanup_strv_free_ char **dropins = NULL;
                _cleanup_free_ char *path = NULL;
                const char *link_config;

                link_config = startswith(*name, "@");
                if (link_config) {
                        _cleanup_free_ char *ifname = NULL;
                        LinkConfigType type;

                        r = parse_link_config(link_config, &ifname, &type);
                        if (r < 0)
                                return r;
                        if (type == _CONFIG_MAX)
                                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                                       "Config type 'all' cannot be used with 'edit'.");

                        r = get_config_files_by_link_config(ifname, type,
                                                            /* ignore_missing= */ false,
                                                            &rtnl,
                                                            &path, &dropins);
                        if (r < 0)
                                return r;

                        r = add_config_to_edit(&context, path, dropins);
                        if (r < 0)
                                return r;

                        reload |= type == CONFIG_LINK ? RELOAD_UDEVD : RELOAD_NETWORKD;

                        continue;
                }

                if (!filename_is_valid(*name))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid config filename: %s", *name);

                if (ENDSWITH_SET(*name, ".network", ".netdev"))
                        reload |= RELOAD_NETWORKD;
                else if (endswith(*name, ".link"))
                        reload |= RELOAD_UDEVD;
                else
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Network config of unknown type: %s", *name);

                r = get_config_files_by_name(*name, /* allow_masked= */ false, &path, &dropins);
                if (r == -ERFKILL)
                        return log_error_errno(r, "Network config '%s' is masked.", *name);
                if (r == -ENOENT) {
                        if (arg_drop_in)
                                return log_error_errno(r, "Cannot find network config '%s'.", *name);

                        log_debug("No existing network config '%s' found, creating a new file.", *name);

                        path = path_join(NETWORK_DIRS[arg_runtime ? 1 : 0], *name);
                        if (!path)
                                return log_oom();

                        r = edit_files_add(&context, path, NULL, NULL);
                        if (r < 0)
                                return r;
                        continue;
                }
                if (r < 0)
                        return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);

                r = add_config_to_edit(&context, path, dropins);
                if (r < 0)
                        return r;
        }

        r = do_edit_files_and_install(&context);
        if (r < 0)
                return r;

        return reload_daemons(reload);
}

static int cat_files_by_link_one(
                const char *ifname,
                LinkConfigType type,
                sd_netlink **rtnl,
                bool ignore_missing,
                bool *first) {

        _cleanup_strv_free_ char **dropins = NULL;
        _cleanup_free_ char *path = NULL;
        int r;

        assert(ifname);
        assert(type >= 0 && type < _CONFIG_MAX);
        assert(rtnl);
        assert(first);

        r = get_config_files_by_link_config(ifname, type, ignore_missing, rtnl, &path, &dropins);
        if (ignore_missing && IN_SET(r, -ENOENT, -ESRCH))
                return 0;
        if (r < 0)
                return r;

        if (!*first)
                putchar('\n');

        r = cat_files(path, dropins, /* flags= */ CAT_FORMAT_HAS_SECTIONS);
        if (r < 0)
                return r;

        *first = false;

        return 0;
}

static int cat_files_by_link_config(const char *link_config, sd_netlink **rtnl, bool *first) {
        _cleanup_free_ char *ifname = NULL;
        LinkConfigType type;
        int r;

        assert(link_config);
        assert(rtnl);
        assert(first);

        r = parse_link_config(link_config, &ifname, &type);
        if (r < 0)
                return r;

        if (type == _CONFIG_MAX) {
                for (LinkConfigType i = 0; i < _CONFIG_MAX; i++) {
                        r = cat_files_by_link_one(ifname, i, rtnl, /* ignore_missing= */ true, first);
                        if (r < 0)
                                return r;
                }

                return 0;
        }

        return cat_files_by_link_one(ifname, type, rtnl, /* ignore_missing= */ false, first);
}

int verb_cat(int argc, char *argv[], uintptr_t _data, void *userdata) {
        _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL;
        char **args = strv_skip(argv, 1);
        int r, ret = 0;

        pager_open(arg_pager_flags);

        if (strv_isempty(args))
                return conf_files_cat(NULL, "systemd/networkd.conf", CAT_FORMAT_HAS_SECTIONS);

        bool first = true;
        STRV_FOREACH(name, args) {
                const char *link_config;

                link_config = startswith(*name, "@");
                if (link_config) {
                        r = cat_files_by_link_config(link_config, &rtnl, &first);
                        if (r < 0)
                                return r;
                        continue;
                }

                if (!filename_is_valid(*name) || !ENDSWITH_SET(*name, ".network", ".netdev", ".link"))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid network config name: %s", *name);

                _cleanup_strv_free_ char **dropins = NULL;
                _cleanup_free_ char *path = NULL;

                r = get_config_files_by_name(*name, /* allow_masked= */ false, &path, &dropins);
                if (r == -ENOENT) {
                        RET_GATHER(ret, log_error_errno(r, "Cannot find network config file '%s'.", *name));
                        continue;
                }
                if (r == -ERFKILL) {
                        RET_GATHER(ret, log_debug_errno(r, "Network config '%s' is masked, ignoring.", *name));
                        continue;
                }
                if (r < 0)
                        return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);

                if (!first)
                        putchar('\n');

                r = cat_files(path, dropins, /* flags= */ CAT_FORMAT_HAS_SECTIONS);
                if (r < 0)
                        return r;

                first = false;
        }

        return ret;
}

int verb_mask(int argc, char *argv[], uintptr_t _data, void *userdata) {
        ReloadFlags flags = 0;
        int r;

        r = mac_selinux_init();
        if (r < 0)
                return r;

        STRV_FOREACH(name, strv_skip(argv, 1)) {
                _cleanup_free_ char *config_path = NULL, *symlink_path = NULL;
                ReloadFlags reload;

                if (!filename_is_valid(*name))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid config filename: %s", *name);

                /* We update the real 'flags' at last, since the operation can be skipped. */
                if (ENDSWITH_SET(*name, ".network", ".netdev"))
                        reload = RELOAD_NETWORKD;
                else if (endswith(*name, ".link"))
                        reload = RELOAD_UDEVD;
                else
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Network config of unknown type: %s", *name);

                r = get_config_files_by_name(*name, /* allow_masked= */ true, &config_path, /* ret_dropins= */ NULL);
                if (r == -ENOENT)
                        log_warning("No existing network config '%s' found, proceeding anyway.", *name);
                else if (r < 0)
                        return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);
                else if (!path_startswith(config_path, "/usr")) {
                        r = null_or_empty_path(config_path);
                        if (r < 0)
                                return log_error_errno(r,
                                                       "Failed to check if '%s' is masked: %m", config_path);
                        if (r > 0) {
                                log_debug("%s is already masked, skipping.", config_path);
                                continue;
                        }

                        /* At this point, we have found a config under mutable dir (/run/ or /etc/),
                         * so masking through /run/ (--runtime) is not possible. If it's under /etc/,
                         * then it doesn't work without --runtime either. */
                        if (arg_runtime || path_startswith(config_path, "/etc"))
                                return log_error_errno(SYNTHETIC_ERRNO(EEXIST),
                                                       "Cannot mask network config %s: %s exists",
                                                       *name, config_path);
                }

                symlink_path = path_join(NETWORK_DIRS[arg_runtime ? 1 : 0], *name);
                if (!symlink_path)
                        return log_oom();

                (void) mkdir_parents_label(symlink_path, 0755);

                if (symlink("/dev/null", symlink_path) < 0)
                        return log_error_errno(errno,
                                               "Failed to create symlink '%s' to /dev/null: %m", symlink_path);

                flags |= reload;
                log_info("Successfully created symlink '%s' to /dev/null.", symlink_path);
        }

        return reload_daemons(flags);
}

int verb_unmask(int argc, char *argv[], uintptr_t _data, void *userdata) {
        ReloadFlags flags = 0;
        int r;

        STRV_FOREACH(name, strv_skip(argv, 1)) {
                _cleanup_free_ char *path = NULL;
                ReloadFlags reload;

                if (!filename_is_valid(*name))
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid config filename: %s", *name);

                if (ENDSWITH_SET(*name, ".network", ".netdev"))
                        reload = RELOAD_NETWORKD;
                else if (endswith(*name, ".link"))
                        reload = RELOAD_UDEVD;
                else
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Network config of unknown type: %s", *name);

                r = get_config_files_by_name(*name, /* allow_masked= */ true, &path, /* ret_dropins= */ NULL);
                if (r == -ENOENT) {
                        log_debug_errno(r, "Network configuration '%s' doesn't exist, skipping.", *name);
                        continue;
                }
                if (r < 0)
                        return log_error_errno(r, "Failed to get the path of network config '%s': %m", *name);

                r = null_or_empty_path(path);
                if (r < 0)
                        return log_error_errno(r, "Failed to check if '%s' is masked: %m", path);
                if (r == 0)
                        continue;

                if (path_startswith(path, "/usr"))
                        return log_error_errno(r, "Cannot unmask network config under /usr/: %s", path);

                if (unlink(path) < 0) {
                        if (errno == ENOENT)
                                continue;

                        return log_error_errno(errno, "Failed to remove '%s': %m", path);
                }

                flags |= reload;
                log_info("Successfully removed masked network config '%s'.", path);
        }

        return reload_daemons(flags);
}
