/* SPDX-License-Identifier: LGPL-2.1+ */

#include "config.h"

#include <arpa/inet.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/sockios.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <netinet/in.h>
#include <pwd.h>
#include <sched.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "compiler.h"
#include "file_utils.h"
#include "log.h"
#include "memory_utils.h"
#include "network.h"
#include "nl.h"
#include "parse.h"
#include "process_utils.h"
#include "string_utils.h"
#include "syscall_wrappers.h"
#include "utils.h"

#if !HAVE_STRLCPY
#include "strlcpy.h"
#endif

#define usernic_debug_stream(stream, format, ...)                              \
	do {                                                                   \
		fprintf(stream, "%s: %d: %s: " format, __FILE__, __LINE__,     \
			__func__, __VA_ARGS__);                                \
	} while (false)

#define usernic_error(format, ...) usernic_debug_stream(stderr, format, __VA_ARGS__)

#define cmd_error_errno(__ret__, __errno__, format, ...)      \
	({                                                    \
		typeof(__ret__) __internal_ret__ = (__ret__); \
		errno = (__errno__);                          \
		CMD_SYSERROR(format, ##__VA_ARGS__);          \
		__internal_ret__;                             \
	})

__noreturn static void usage(bool fail)
{
	fprintf(stderr, "Description:\n");
	fprintf(stderr, "  Manage nics in another network namespace\n\n");

	fprintf(stderr, "Usage:\n");
	fprintf(stderr, "  lxc-user-nic [command]\n\n");

	fprintf(stderr, "Available Commands:\n");
	fprintf(stderr, "  create {lxcpath} {name} {pid} {type} {bridge} {container nicname}\n");
	fprintf(stderr, "  delete {lxcpath} {name} {/proc/<pid>/ns/net} {type} {bridge} {container nicname}\n");

	if (fail)
		_exit(EXIT_FAILURE);

	_exit(EXIT_SUCCESS);
}

static int open_and_lock(const char *path)
{
	__do_close int fd = -EBADF;
	int ret;
	struct flock lk;

	fd = open(path, O_RDWR | O_CREAT, S_IWUSR | S_IRUSR | O_CLOEXEC);
	if (fd < 0) {
		CMD_SYSERROR("Failed to open \"%s\"\n", path);
		return -1;
	}

	lk.l_type = F_WRLCK;
	lk.l_whence = SEEK_SET;
	lk.l_start = 0;
	lk.l_len = 0;

	ret = fcntl(fd, F_SETLKW, &lk);
	if (ret < 0) {
		CMD_SYSERROR("Failed to lock \"%s\"\n", path);
		return -1;
	}

	return move_fd(fd);
}

static char *get_username(void)
{
	__do_free char *buf = NULL;
	struct passwd pwent;
	struct passwd *pwentp = NULL;
	ssize_t bufsize;
	int ret;

	bufsize = sysconf(_SC_GETPW_R_SIZE_MAX);
	if (bufsize < 0)
		bufsize = 1024;

	buf = malloc(bufsize);
	if (!buf)
		return NULL;

	ret = getpwuid_r(getuid(), &pwent, buf, bufsize, &pwentp);
	if (!pwentp) {
		if (ret == 0)
			usernic_error("%s", "Could not find matched password record\n");

		CMD_SYSERROR("Failed to get username: %u\n", getuid());
		return NULL;
	}

	return strdup(pwent.pw_name);
}


static char **get_groupnames(void)
{
	__do_free char *buf = NULL;
	__do_free gid_t *group_ids = NULL;
	__do_free_string_list char **groupnames = NULL;
	int ngroups;
	int ret, i;
	struct group grent;
	struct group *grentp = NULL;
	ssize_t bufsize;

	ngroups = getgroups(0, NULL);
	if (ngroups < 0) {
		CMD_SYSERROR("Failed to get number of groups the user belongs to\n");
		return NULL;
	}

	if (ngroups == 0)
		return NULL;

	group_ids = malloc(sizeof(gid_t) * ngroups);
	if (!group_ids) {
		CMD_SYSERROR("Failed to allocate memory while getting groups the user belongs to\n");
		return NULL;
	}

	ret = getgroups(ngroups, group_ids);
	if (ret < 0) {
		CMD_SYSERROR("Failed to get process groups\n");
		return NULL;
	}

	groupnames = zalloc(sizeof(char *) * (ngroups + 1));
	if (!groupnames) {
		CMD_SYSERROR("Failed to allocate memory while getting group names\n");
		return NULL;
	}

	bufsize = sysconf(_SC_GETGR_R_SIZE_MAX);
	if (bufsize < 0)
		bufsize = 1024;

	buf = malloc(bufsize);
	if (!buf) {
		CMD_SYSERROR("Failed to allocate memory while getting group names\n");
		return NULL;
	}

	for (i = 0; i < ngroups; i++) {
		while ((ret = getgrgid_r(group_ids[i], &grent, buf, bufsize, &grentp)) == ERANGE) {
			char *new_buf;

			bufsize <<= 1;
			if (bufsize > MAX_GRBUF_SIZE) {
				usernic_error("Failed to get group members: %u\n", group_ids[i]);
				return NULL;
			}

			new_buf = realloc(buf, bufsize);
			if (!new_buf) {
				usernic_error("Failed to allocate memory while getting group names: %s\n",
					      strerror(errno));
				return NULL;
			}
			buf = new_buf;
		}

		/* If a group is not found, just ignore it. */
		if (!grentp)
			continue;

		groupnames[i] = strdup(grent.gr_name);
		if (!groupnames[i]) {
			usernic_error("Failed to copy group name \"%s\"", grent.gr_name);
			return NULL;
		}
	}

	return move_ptr(groupnames);
}

static bool name_is_in_groupnames(char *name, char **groupnames)
{
	if (!groupnames)
		return false;

	while (*groupnames) {
		if (!strcmp(name, *groupnames))
			return true;
		groupnames++;
	}

	return false;
}

struct alloted_s {
	char *name;
	int allowed;
	struct alloted_s *next;
};

static struct alloted_s *append_alloted(struct alloted_s **head, char *name, int n)
{
	__do_free struct alloted_s *al = NULL;
	struct alloted_s *cur;

	if (!head || !name) {
		/* Sanity check. Parameters should not be null. */
		usernic_error("%s\n", "Unexpected NULL argument");
		return NULL;
	}

	al = zalloc(sizeof(struct alloted_s));
	if (!al) {
		CMD_SYSERROR("Failed to allocate memory\n");
		return NULL;
	}

	al->name = strdup(name);
	if (!al->name)
		return NULL;

	al->allowed = n;
	al->next = NULL;

	if (*head) {
		cur = *head;
		while (cur->next)
			cur = cur->next;
		cur->next = al;
	} else {
		*head = al;
	}

	return move_ptr(al);
}

static void free_alloted(struct alloted_s **head)
{
	struct alloted_s *cur;

	if (!head)
		return;

	cur = *head;
	while (cur) {
		cur = cur->next;
		free((*head)->name);
		free(*head);
		*head = cur;
	}
}

/* The configuration file consists of lines of the form:
 *
 * user type bridge count
 * or
 * @group type bridge count
 *
 * Return the count entry for the calling user if there is one.  Else
 * return -1.
 */
static int get_alloted(char *me, char *intype, char *link,
		       struct alloted_s **alloted)
{
	__do_free char *line = NULL;
	__do_fclose FILE *fin = NULL;
	__do_free_string_list char **groups = NULL;
	int n, ret;
	char name[100], type[100], br[100];
	int count = 0;
	size_t len = 0;

	fin = fopen(LXC_USERNIC_CONF, "re");
	if (!fin) {
		CMD_SYSERROR("Failed to open \"%s\"\n", LXC_USERNIC_CONF);
		return -1;
	}

	groups = get_groupnames();
	while ((getline(&line, &len, fin)) != -1) {
		ret = sscanf(line, "%99[^ \t] %99[^ \t] %99[^ \t] %d", name,
			     type, br, &n);
		if (ret != 4)
			continue;

		if (is_empty_string(name))
			continue;

		if (!strequal(name, me)) {
			if (name[0] != '@')
				continue;

			if (!name_is_in_groupnames(name + 1, groups))
				continue;
		}

		if (!strequal(type, intype))
			continue;

		if (!strequal(link, br))
			continue;

		/*
		 * Found the user or group with the appropriate settings,
		 * therefore finish the search. What to do if there are is more
		 * than one applicable line? Currently this is not specified in
		 * the docs.
		 *
		 * If append_alloted returns NULL, e.g. due to a malloc error,
		 * we set count to 0 and break the loop, allowing cleanup and
		 * then exiting from main().
		 */
		if (!append_alloted(alloted, name, n)) {
			count = 0;
			break;
		}

		count += n;
	}

	/* Now return the total number of nics that this user can create. */
	return count;
}

static char *get_eol(char *s, char *e)
{
	while ((s < e) && *s && (*s != '\n'))
		s++;

	return s;
}

static char *get_eow(char *s, char *e)
{
	while ((s < e) && *s && !isblank(*s) && (*s != '\n'))
		s++;

	return s;
}

static bool same_word(const char *start, const char *end, const char *word)
{
	size_t wordlen = strlen(word);
	size_t buflen = end - start;

	if (wordlen != buflen)
		return false;
	if (strncmp(start, word, wordlen) == 0)
		return true;
	return false;
}

/*
 * in:
 * @buf_start and @buf_end point to the buffer to be read.
 *
 * @owner_name is the name of the user who should own the link.
 *
 * @net_type is type of connection, e.g. veth
 *
 * @net_link is the name of the bridge, e.g. lxcbr0, on which the
 * device should live.
 *
 * @net_dev is the name of the device itself in the host netns.
 *
 * out:
 * @is_owner is set to true if the current line is owned by @name.

 * @nic_found is set to true if the line is specifically for the passed-in
 * @net_dev, and it is on the right @net_link and of the right @net_type.
 *
 * @exists is set to false if the nic in this line no longer exists.  This is
 * used by cull_entries(): if we set it to false, then this line will be
 * removed from the LXC_USERNIC_DB (e.g. /var/run/lxc/nics).
 */
static char *find_line(char *buf_start, char *buf_end, char *name,
		       char *net_type, char *net_link, char *net_dev,
		       bool *is_owner, bool *nic_found, bool *exists)
{
	char *end_of_line, *end_of_word, *line;
	bool right_net_type, right_bridge, right_link_name;;

	while (buf_start < buf_end) {
		size_t len;
		char netdev_name[IFNAMSIZ];

		*nic_found = false;
		*exists = true;
		*is_owner = false;
		right_net_type  = false;
		right_bridge    = false;
		right_link_name = false;

		end_of_line = get_eol(buf_start, buf_end);
		if (end_of_line >= buf_end)
			return NULL;

		line = buf_start;
		if (*buf_start == '#')
			goto next;

		while ((buf_start < buf_end) && isblank(*buf_start))
			buf_start++;

		/* Check whether the line contains the caller's name. */
		end_of_word = get_eow(buf_start, buf_end);
		/* corrupt db */
		if (!end_of_word)
			return NULL;

		if (same_word(buf_start, end_of_word, name))
			*is_owner = true;

		buf_start = end_of_word + 1;
		while ((buf_start < buf_end) && isblank(*buf_start))
			buf_start++;

		/* Check whether line is of the right network type. */
		end_of_word = get_eow(buf_start, buf_end);
		/* corrupt db */
		if (!end_of_word)
			return NULL;

		if (same_word(buf_start, end_of_word, net_type))
			right_net_type = true;

		buf_start = end_of_word + 1;
		while ((buf_start < buf_end) && isblank(*buf_start))
			buf_start++;

		/* Check whether line is contains the right link. */
		end_of_word = get_eow(buf_start, buf_end);
		/* corrupt db */
		if (!end_of_word)
			return NULL;

		if (same_word(buf_start, end_of_word, net_link))
			right_bridge = true;

		buf_start = end_of_word + 1;
		while ((buf_start < buf_end) && isblank(*buf_start))
			buf_start++;

		/* Check whether line contains the right network device. */
		end_of_word = get_eow(buf_start, buf_end);
		/* corrupt db */
		if (!end_of_word)
			return NULL;

		len = end_of_word - buf_start;
		/* corrupt db */
		if (len >= IFNAMSIZ)
			return NULL;

		memcpy(netdev_name, buf_start, len);
		netdev_name[len] = '\0';
		*exists = lxc_nic_exists(netdev_name);

		if (net_dev && !strcmp(netdev_name, net_dev))
			right_link_name = true;

		if (right_net_type && right_bridge && right_link_name)
			*nic_found = true;

		return line;

	next:
		buf_start = end_of_line + 1;
	}

	return NULL;
}

static int instantiate_veth(char *veth1, char *veth2, pid_t pid, unsigned int mtu)
{
	int ret;

	ret = lxc_veth_create(veth1, veth2, pid, mtu, -1, -1);
	if (ret < 0) {
		CMD_SYSERROR("Failed to create %s-%s\n", veth1, veth2);
		return ret_errno(-ret);
	}

	/*
	 * Changing the high byte of the mac address to 0xfe, the bridge
	 * interface will always keep the host's mac address and not take the
	 * mac address of a container.
	 */
	ret = setup_private_host_hw_addr(veth1);
	if (ret < 0) {
		CMD_SYSERROR("Failed to change mac address of host interface %s\n", veth1);
		return ret_errno(-ret);
	}

	return netdev_set_flag(veth1, IFF_UP);
}

#define NETDEV_MTU_DEFAULT 1500

static unsigned int get_mtu(char *name)
{
	int idx, val;

	idx = if_nametoindex(name);
	if (idx < 0) {
		usernic_error("Could not find netdev %s\n", name);
		return NETDEV_MTU_DEFAULT;
	}

	val = netdev_get_mtu(idx);
	if (val < 0) {
		usernic_error("Could not get MTU for netdev %s ifindex %d\n", name, idx);
		return NETDEV_MTU_DEFAULT;
	}

	return val;
}

static int create_nic(char *nic, char *br, int pid, char **cnic)
{
	unsigned int mtu = NETDEV_MTU_DEFAULT;
	int ret;
	char veth1buf[IFNAMSIZ], veth2buf[IFNAMSIZ];

	ret = snprintf(veth1buf, IFNAMSIZ, "%s", nic);
	if (ret < 0 || ret >= IFNAMSIZ) {
		usernic_error("%s", "Could not create nic name\n");
		return -1;
	}

	ret = snprintf(veth2buf, IFNAMSIZ, "%sp", veth1buf);
	if (ret < 0 || ret >= IFNAMSIZ) {
		usernic_error("%s\n", "Could not create nic name");
		return -1;
	}

	if (strcmp(br, "none"))
		mtu = get_mtu(br);

	/* create the nics */
	ret = instantiate_veth(veth1buf, veth2buf, pid, mtu);
	if (ret < 0) {
		usernic_error("%s", "Error creating veth tunnel\n");
		return -1;
	}

	if (strcmp(br, "none")) {
		if (mtu > 0) {
			ret = lxc_netdev_set_mtu(veth1buf, mtu);
			if (ret < 0) {
				usernic_error("Failed to set mtu to %d on %s\n",
					      mtu, veth1buf);
				goto out_del;
			}
		}

		/* attach veth1 to bridge */
		ret = lxc_bridge_attach(br, veth1buf);
		if (ret < 0) {
			usernic_error("Error attaching %s to %s\n", veth1buf, br);
			goto out_del;
		}
	}

	*cnic = strdup(veth2buf);
	if (!*cnic) {
		usernic_error("Failed to copy string \"%s\"\n", veth2buf);
		return -1;
	}

	return 0;

out_del:
	lxc_netdev_delete_by_name(veth1buf);
	return -1;
}

struct entry_line {
	char *start;
	int len;
	bool keep;
};

static bool cull_entries(int fd, char *name, char *net_type, char *net_link,
			 char *net_dev, bool *found_nicname)
{
	__do_free char *buf = NULL;
	__do_free struct entry_line *entry_lines = NULL;
	int n = 0;
	size_t length = 0;
	int ret;
	char *buf_end, *buf_start;
	bool nic_found, is_owner, keep;

	ret = fd_to_buf(fd, &buf, &length);
	if (ret < 0) {
		CMD_SYSERROR("Failed to read database file\n");
		return false;
	}
	if (lseek(fd, 0, SEEK_SET) < 0)
		return false;

	if (length == 0)
		return false;

	buf_start = buf;
	buf_end = buf + length;
	while ((buf_start = find_line(buf_start, buf_end, name, net_type,
				      net_link, net_dev, &is_owner, &nic_found,
				      &keep))) {
		struct entry_line *newe;

		newe = realloc(entry_lines, sizeof(*entry_lines) * (n + 1));
		if (!newe)
			return false;

		if (nic_found && is_owner)
			*found_nicname = true;

		entry_lines = newe;
		entry_lines[n].start = buf_start;
		entry_lines[n].len = get_eol(buf_start, buf_end) - entry_lines[n].start;
		entry_lines[n].keep = keep;
		n++;

		buf_start += entry_lines[n - 1].len + 1;
		if (buf_start >= buf_end)
			break;
	}

	buf_start = buf;

	for (int i = 0; i < n; i++) {
		if (!entry_lines[i].keep)
			continue;

		memcpy(buf_start, entry_lines[i].start, entry_lines[i].len);
		buf_start += entry_lines[i].len;
		*buf_start = '\n';
		buf_start++;
	}

	return ftruncate(fd, buf_start - buf) == 0;
}

static int count_entries(char *buf, off_t len, char *name, char *net_type, char *net_link)
{
	int count = 0;
	bool owner = false;
	char *buf_end;

	buf_end = &buf[len];
	while ((buf = find_line(buf, buf_end, name, net_type, net_link, NULL,
				&owner, &(bool){true}, &(bool){true}))) {
		if (owner)
			count++;

		buf = get_eol(buf, buf_end) + 1;
		if (buf >= buf_end)
			break;
	}

	return count;
}

/* The dbfile has lines of the format: user type bridge nicname. */
static char *get_nic_if_avail(int fd, struct alloted_s *names, int pid,
			      char *intype, char *br, int allowed, char **cnic)
{
	__do_free char *buf = NULL, *newline = NULL;
	size_t length = 0;
	int ret;
	size_t slen;
	ssize_t nbytes;
	char *owner;
	char nicname[IFNAMSIZ];
	struct alloted_s *n;
	uid_t uid;

	for (n = names; n != NULL; n = n->next)
		cull_entries(fd, n->name, intype, br, NULL, NULL);

	if (allowed == 0)
		return NULL;

	owner = names->name;

	ret = fd_to_buf(fd, &buf, &length);
	if (ret < 0) {
		CMD_SYSERROR("Failed to read database file\n");
		return false;
	}
	if (lseek(fd, 0, SEEK_SET) < 0)
		return false;

	if (length > 0) {
		owner = NULL;

		for (n = names; n != NULL; n = n->next) {
			int count;

			count = count_entries(buf, length, n->name, intype, br);
			if (count >= n->allowed)
				continue;

			owner = n->name;
			break;
		}
	}

	if (!owner)
		return NULL;

        uid = getuid();
	/*
	 * For POSIX integer uids the network device name schema is
	 * vethUID_XXXX.
	 * With four random characters passed to
	 * lxc_ifname_alnum_case_sensitive() we get 62^4 = 14776336
	 * combinations per uid. That's plenty of network devices for now.
	 */
	if (uid > 0 && uid <= 65536)
		ret = snprintf(nicname, sizeof(nicname), "veth%d_XXXX", uid);
	else
		ret = snprintf(nicname, sizeof(nicname), "vethXXXXXX");
	if (ret < 0 || (size_t)ret >= sizeof(nicname))
		return NULL;

	if (!lxc_ifname_alnum_case_sensitive(nicname))
		return NULL;

	ret = create_nic(nicname, br, pid, cnic);
	if (ret < 0) {
		usernic_error("%s", "Failed to create new nic\n");
		return NULL;
	}

	/* strlen(owner)
	 * +
	 * " "
	 * +
	 * strlen(intype)
	 * +
	 * " "
	 * +
	 * strlen(br)
	 * +
	 * " "
	 * +
	 * strlen(nicname)
	 * +
	 * \n
	 * +
	 * \0
	 */
	slen = strlen(owner) + strlen(intype) + strlen(br) + strlen(nicname) + 4;
	newline = malloc(slen + 1);
	if (!newline) {
		CMD_SYSERROR("Failed allocate memory\n");
		return NULL;
	}

	ret = snprintf(newline, slen + 1, "%s %s %s %s\n", owner, intype, br, nicname);
	if (ret < 0 || (size_t)ret >= (slen + 1)) {
		if (lxc_netdev_delete_by_name(nicname) != 0)
			usernic_error("Error unlinking %s\n", nicname);

		return NULL;
	}

	nbytes = lxc_pwrite_nointr(fd, newline, slen, length);
	if (nbytes < 0 || (size_t)nbytes != slen) {
		CMD_SYSERROR("Failed to append new entry \"%s\" to database file", newline);

		if (lxc_netdev_delete_by_name(nicname) != 0)
			usernic_error("Error unlinking %s\n", nicname);

		return NULL;
	}

	ret = ftruncate(fd, length + slen);
	if (ret < 0) {
		CMD_SYSERROR("Failed to truncate file\n");

		if (lxc_netdev_delete_by_name(nicname) != 0)
			usernic_error("Error unlinking %s\n", nicname);

		return NULL;
	}

	return strdup(nicname);
}

static bool create_db_dir(char *fnam)
{
	__do_free char *copy = NULL;
	char *p;
	int ret;

	copy = must_copy_string(fnam);
	p = copy;
	fnam = p;
	p = p + 1;

again:
	while (*p && *p != '/')
		p++;

	if (!*p)
		return true;

	*p = '\0';

	ret = mkdir(fnam, 0755);
	if (ret < 0 && errno != EEXIST) {
		CMD_SYSERROR("Failed to create %s\n", fnam);
		*p = '/';
		return false;
	}

	*(p++) = '/';

	goto again;
}

static char *lxc_secure_rename_in_ns(int pid, char *oldname, char *newname,
				     int *container_veth_ifidx)
{
	__do_close int fd = -EBADF, ofd = -EBADF;
	int fret = -1;
	int ifindex, ret;
	pid_t pid_self;
	uid_t ruid, suid, euid;
	char ifname[IFNAMSIZ];

	pid_self = lxc_raw_getpid();

	ofd = lxc_preserve_ns(pid_self, "net");
	if (ofd < 0)
		return cmd_error_errno(NULL, errno, "Failed opening network namespace path for %d", pid_self);

	fd = lxc_preserve_ns(pid, "net");
	if (fd < 0)
		return cmd_error_errno(NULL, errno, "Failed opening network namespace path for %d", pid);

	ret = getresuid(&ruid, &euid, &suid);
	if (ret < 0)
		return cmd_error_errno(NULL, errno, "Failed to retrieve real, effective, and saved user IDs\n");

	ret = setns(fd, CLONE_NEWNET);
	if (ret < 0)
		return cmd_error_errno(NULL, errno, "Failed to setns() to the network namespace of the container with PID %d\n", pid);

	ret = setresuid(ruid, ruid, 0);
	if (ret < 0) {
		CMD_SYSERROR("Failed to drop privilege by setting effective user id and real user id to %d, and saved user ID to 0\n", ruid);
		/*
		 * It's ok to jump to do_full_cleanup here since setresuid()
		 * will succeed when trying to set real, effective, and saved
		 * to values they currently have.
		 */
		goto out_setns;
	}

	/* Check if old interface exists. */
	ifindex = if_nametoindex(oldname);
	if (!ifindex) {
		CMD_SYSERROR("Failed to get netdev index\n");
		goto out_setresuid;
	}

	/*
	 * When the IFLA_IFNAME attribute is passed something like "<prefix>%d"
	 * netlink will replace the format specifier with an appropriate index.
	 * So we pass "eth%d".
	 */
	ret = lxc_netdev_rename_by_name(oldname, newname ? newname : "eth%d");
	if (ret < 0) {
		CMD_SYSERROR("Error %d renaming netdev %s to %s in container\n", ret, oldname, newname ? newname : "eth%d");
		goto out_setresuid;
	}

	/* Retrieve new name for interface. */
	if (!if_indextoname(ifindex, ifname)) {
		CMD_SYSERROR("Failed to get new netdev name\n");
		goto out_setresuid;
	}

	fret = 0;

out_setresuid:
	ret = setresuid(ruid, euid, suid);
	if (ret < 0)
		return cmd_error_errno(NULL, errno, "Failed to restore privilege by setting effective user id to %d, real user id to %d, and saved user ID to %d\n",
				       ruid, euid, suid);

out_setns:
	ret = setns(ofd, CLONE_NEWNET);
	if (ret < 0)
		return cmd_error_errno(NULL, errno, "Failed to setns() to original network namespace of PID %d\n", ofd);

	if (fret < 0)
		return NULL;

	*container_veth_ifidx = ifindex;
	return strdup(ifname);
}

/* If the caller (real uid, not effective uid) may read the /proc/[pid]/ns/net,
 * then it is either the caller's netns or one which it created.
 */
static bool may_access_netns(int pid)
{
	int ret;
	char s[200];
	uid_t ruid, suid, euid;
	bool may_access = false;

	ret = getresuid(&ruid, &euid, &suid);
	if (ret < 0) {
		CMD_SYSERROR("Failed to retrieve real, effective, and saved user IDs\n");
		return false;
	}

	ret = setresuid(ruid, ruid, euid);
	if (ret < 0) {
		CMD_SYSERROR("Failed to drop privilege by setting effective user id and real user id to %d, and saved user ID to %d\n",
			     ruid, euid);
		return false;
	}

	ret = snprintf(s, 200, "/proc/%d/ns/net", pid);
	if (ret < 0 || ret >= 200)
		return false;

	ret = access(s, R_OK);
	may_access = true;
	if (ret < 0) {
		may_access = false;
		CMD_SYSERROR("Uid %d may not access %s\n", (int)ruid, s);
	}

	ret = setresuid(ruid, euid, suid);
	if (ret < 0) {
		CMD_SYSERROR("Failed to restore user id to %d, real user id to %d, and saved user ID to %d\n",
			     ruid, euid, suid);
		may_access = false;
	}

	return may_access;
}

struct user_nic_args {
	char *cmd;
	char *lxc_path;
	char *lxc_name;
	char *pid;
	char *type;
	char *link;
	char *veth_name;
};

enum lxc_user_nic_command {
	LXC_USERNIC_CREATE = 0,
	LXC_USERNIC_DELETE = 1,
};

static bool is_privileged_over_netns(int netns_fd)
{
	int ofd, ret;
	pid_t pid_self;
	uid_t euid, ruid, suid;
	bool bret = false;

	pid_self = lxc_raw_getpid();

	ofd = lxc_preserve_ns(pid_self, "net");
	if (ofd < 0) {
		usernic_error("Failed opening network namespace path for %d", pid_self);
		return false;
	}

	ret = getresuid(&ruid, &euid, &suid);
	if (ret < 0) {
		CMD_SYSERROR("Failed to retrieve real, effective, and saved user IDs\n");
		goto do_partial_cleanup;
	}

	ret = setns(netns_fd, CLONE_NEWNET);
	if (ret < 0) {
		CMD_SYSERROR("Failed to setns() to network namespace\n");
		goto do_partial_cleanup;
	}

	ret = setresuid(ruid, ruid, 0);
	if (ret < 0) {
		CMD_SYSERROR("Failed to drop privilege by setting effective user id and real user id to %d, and saved user ID to 0\n",
			     ruid);
		/* It's ok to jump to do_full_cleanup here since setresuid()
		 * will succeed when trying to set real, effective, and saved to
		 * values they currently have.
		 */
		goto do_full_cleanup;
	}

	/* Test whether we are privileged over the network namespace. To do this
	 * we try to delete the loopback interface which is not possible. If we
	 * are privileged over the network namespace we will get ENOTSUP. If we
	 * are not privileged over the network namespace we will get EPERM.
	 */
	ret = lxc_netdev_delete_by_name("lo");
	if (ret == -ENOTSUP)
		bret = true;

do_full_cleanup:
	ret = setresuid(ruid, euid, suid);
	if (ret < 0) {
		CMD_SYSERROR("Failed to restore privilege by setting effective user id to %d, real user id to %d, and saved user ID to %d\n",
			     ruid, euid, suid);
		bret = false;
	}

	ret = setns(ofd, CLONE_NEWNET);
	if (ret < 0) {
		CMD_SYSERROR("Failed to setns() to original network namespace of PID %d\n",
			     ofd);
		bret = false;
	}

do_partial_cleanup:
	close(ofd);
	return bret;
}

static inline int validate_args(const struct user_nic_args *args, int argc)
{
	int request = -EINVAL;

	if (!strcmp(args->cmd, "create"))
		request = LXC_USERNIC_CREATE;
	else if (!strcmp(args->cmd, "delete"))
		request = LXC_USERNIC_DELETE;

	return request;
}

int main(int argc, char *argv[])
{
	__do_free char *me = NULL, *newname = NULL, *nicname = NULL;
	int fd, n, pid, request, ret;
	struct user_nic_args args;
	int container_veth_ifidx = -1, host_veth_ifidx = -1, netns_fd = -1;
	char *cnic = NULL;
	struct alloted_s *alloted = NULL;

	if (argc < 7 || argc > 8)
		usage(true);

	memset(&args, 0, sizeof(struct user_nic_args));

	args.cmd = argv[1];
	args.lxc_path = argv[2];
	args.lxc_name = argv[3];
	args.pid = argv[4];
	args.type = argv[5];
	args.link = argv[6];
	if (argc == 8)
		args.veth_name = argv[7];

	request = validate_args(&args, argc);
	if (request < 0)
		usage(true);

	/* Set a sane env, because we are setuid-root. */
	ret = clearenv();
	if (ret) {
		usernic_error("%s", "Failed to clear environment\n");
		_exit(EXIT_FAILURE);
	}

	ret = setenv("PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 1);
	if (ret < 0) {
		usernic_error("%s", "Failed to set PATH, exiting\n");
		_exit(EXIT_FAILURE);
	}

	me = get_username();
	if (!me) {
		usernic_error("%s", "Failed to get username\n");
		_exit(EXIT_FAILURE);
	}

	if (request == LXC_USERNIC_CREATE) {
		ret = lxc_safe_int(args.pid, &pid);
		if (ret < 0) {
			usernic_error("Could not read pid: %s\n", args.pid);
			_exit(EXIT_FAILURE);
		}
	} else if (request == LXC_USERNIC_DELETE) {
		char opath[LXC_PROC_PID_FD_LEN];

		// Keep in mind CVE-2022-47952: It's crucial not to leak any
		// information whether open() succeeded of failed.

		netns_fd = open(args.pid, O_PATH | O_CLOEXEC);
		if (netns_fd < 0) {
			usernic_error("Failed while opening netns file for \"%s\"\n", args.pid);
			_exit(EXIT_FAILURE);
		}

		if (!fhas_fs_type(netns_fd, NSFS_MAGIC)) {
			usernic_error("Failed while opening netns file for \"%s\"\n", args.pid);
			close(netns_fd);
			_exit(EXIT_FAILURE);
		}

		ret = snprintf(opath, sizeof(opath), "/proc/self/fd/%d", netns_fd);
		if (ret < 0 || (size_t)ret >= sizeof(opath)) {
			close(netns_fd);
			_exit(EXIT_FAILURE);
		}

		/* Now get an fd that we can use in setns() calls. */
		ret = open(opath, O_RDONLY | O_CLOEXEC);
		if (ret < 0) {
			CMD_SYSERROR("Failed while opening netns file for \"%s\"\n", args.pid);
			close(netns_fd);
			_exit(EXIT_FAILURE);
		}

		close(netns_fd);
		netns_fd = ret;
	}

	if (!create_db_dir(LXC_USERNIC_DB)) {
		usernic_error("%s", "Failed to create directory for db file\n");

		if (netns_fd >= 0)
			close(netns_fd);

		_exit(EXIT_FAILURE);
	}

	fd = open_and_lock(LXC_USERNIC_DB);
	if (fd < 0) {
		usernic_error("Failed to lock %s\n", LXC_USERNIC_DB);

		if (netns_fd >= 0)
			close(netns_fd);

		_exit(EXIT_FAILURE);
	}

	if (request == LXC_USERNIC_CREATE) {
		if (!may_access_netns(pid)) {
			usernic_error("User %s may not modify netns for pid %d\n", me, pid);
			_exit(EXIT_FAILURE);
		}
	} else if (request == LXC_USERNIC_DELETE) {
		bool has_priv;

		has_priv = is_privileged_over_netns(netns_fd);
		close(netns_fd);
		if (!has_priv) {
			usernic_error("%s", "Process is not privileged over network namespace\n");
			_exit(EXIT_FAILURE);
		}
	}

	n = get_alloted(me, args.type, args.link, &alloted);

	if (request == LXC_USERNIC_DELETE) {
		struct alloted_s *it;
		bool found_nicname = false;

		if (!is_ovs_bridge(args.link)) {
			usernic_error("%s", "Deletion of non ovs type network devices not implemented\n");
			close(fd);
			free_alloted(&alloted);
			_exit(EXIT_FAILURE);
		}

		/* Check whether the network device we are supposed to delete
		 * exists in the db. If it doesn't we will not delete it as we
		 * need to assume the network device is not under our control.
		 * As a side effect we also clear any invalid entries from the
		 * database.
		 */
		for (it = alloted; it; it = it->next)
			cull_entries(fd, it->name, args.type, args.link,
				     args.veth_name, &found_nicname);
		close(fd);
		free_alloted(&alloted);

		if (!found_nicname) {
			usernic_error("Caller is not allowed to delete network device \"%s\"\n", args.veth_name);
			_exit(EXIT_FAILURE);
		}

		ret = lxc_ovs_delete_port(args.link, args.veth_name);
		if (ret < 0) {
			usernic_error("Failed to remove port \"%s\" from openvswitch bridge \"%s\"", args.veth_name, args.link);
			_exit(EXIT_FAILURE);
		}

		_exit(EXIT_SUCCESS);
	}

	if (n > 0)
		nicname = get_nic_if_avail(fd, alloted, pid, args.type,
					   args.link, n, &cnic);

	close(fd);
	free_alloted(&alloted);

	if (!nicname) {
		usernic_error("%s", "Quota reached\n");
		_exit(EXIT_FAILURE);
	}

	/* Now rename the link. */
	newname = lxc_secure_rename_in_ns(pid, cnic, args.veth_name,
					  &container_veth_ifidx);
	if (!newname) {
		usernic_error("%s", "Failed to rename the link\n");

		ret = lxc_netdev_delete_by_name(cnic);
		if (ret < 0)
			usernic_error("Failed to delete \"%s\"\n", cnic);

		_exit(EXIT_FAILURE);
	}

	host_veth_ifidx = if_nametoindex(nicname);
	if (!host_veth_ifidx) {
		CMD_SYSERROR("Failed to get netdev index\n");
		_exit(EXIT_FAILURE);
	}

	/* Write names of veth pairs and their ifindices to stout:
	 * (e.g. eth0:731:veth9MT2L4:730)
	 */
	fprintf(stdout, "%s:%d:%s:%d\n", newname, container_veth_ifidx, nicname,
		host_veth_ifidx);

	fflush(stdout);
	_exit(EXIT_SUCCESS);
}
