/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
 * Copyright (c) 2008 Vincent Bernat <bernat@luffy.cx>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

/* This file contains code for privilege separation. When an error arises in
 * monitor (which is running as root), it just stops instead of trying to
 * recover. This module also contains proxies to privileged operations. In this
 * case, error can be non fatal. */

#include "lldpd.h"
#include "trace.h"

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <grp.h>
#include <sys/utsname.h>
#include <sys/ioctl.h>
#include <netinet/if_ether.h>

#if defined HOST_OS_FREEBSD || HOST_OS_OSX || HOST_OS_DRAGONFLY
# include <net/if_dl.h>
#endif
#if defined HOST_OS_SOLARIS
# include <sys/sockio.h>
#endif

/* Use resolv.h */
#ifdef HAVE_SYS_TYPES_H
#  include <sys/types.h>
#endif
#ifdef HAVE_NETINET_IN_H
#  include <netinet/in.h>
#endif
#ifdef HAVE_ARPA_NAMESER_H
#  include <arpa/nameser.h> /* DNS HEADER struct */
#endif
#ifdef HAVE_NETDB_H
#  include <netdb.h>
#endif
#ifdef HAVE_RESOLV_H
#  include <resolv.h>
#endif

/* Bionic has res_init() but it's not in any header */
#if defined HAVE_RES_INIT && defined __BIONIC__
int res_init (void);
#endif

#ifdef ENABLE_PRIVSEP
static int monitored = -1;		/* Child */
#endif

/* Proxies */
static void
priv_ping()
{
	int rc;
	enum priv_cmd cmd = PRIV_PING;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	log_debug("privsep", "monitor ready");
}

/* Proxy for ctl_cleanup */
void
priv_ctl_cleanup(const char *ctlname)
{
	int rc, len = strlen(ctlname);
	enum priv_cmd cmd = PRIV_DELETE_CTL_SOCKET;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, &len, sizeof(int));
	must_write(PRIV_UNPRIVILEGED, ctlname, len);
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
}

/* Proxy for gethostname */
char *
priv_gethostname()
{
	static char *buf = NULL;
	int rc;
	enum priv_cmd cmd = PRIV_GET_HOSTNAME;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	if ((buf = (char*)realloc(buf, rc+1)) == NULL)
		fatal("privsep", NULL);
	must_read(PRIV_UNPRIVILEGED, buf, rc);
	buf[rc] = '\0';
	return buf;
}


int
priv_iface_init(int index, char *iface)
{
	int rc;
	char dev[IFNAMSIZ] = {};
	enum priv_cmd cmd = PRIV_IFACE_INIT;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, &index, sizeof(int));
	strlcpy(dev, iface, IFNAMSIZ);
	must_write(PRIV_UNPRIVILEGED, dev, IFNAMSIZ);
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	if (rc != 0) return -1;
	return receive_fd(PRIV_UNPRIVILEGED);
}

int
priv_iface_multicast(const char *name, const u_int8_t *mac, int add)
{
	int rc;
	enum priv_cmd cmd = PRIV_IFACE_MULTICAST;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, name, IFNAMSIZ);
	must_write(PRIV_UNPRIVILEGED, mac, ETHER_ADDR_LEN);
	must_write(PRIV_UNPRIVILEGED, &add, sizeof(int));
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	return rc;
}

int
priv_iface_description(const char *name, const char *description)
{
	int rc, len = strlen(description);
	enum priv_cmd cmd = PRIV_IFACE_DESCRIPTION;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, name, IFNAMSIZ);
	must_write(PRIV_UNPRIVILEGED, &len, sizeof(int));
	must_write(PRIV_UNPRIVILEGED, description, len);
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	return rc;
}

/* Proxy to set interface in promiscuous mode */
int
priv_iface_promisc(const char *ifname)
{
	int rc;
	enum priv_cmd cmd = PRIV_IFACE_PROMISC;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, ifname, IFNAMSIZ);
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	return rc;
}

int
priv_snmp_socket(struct sockaddr_un *addr)
{
	int rc;
	enum priv_cmd cmd = PRIV_SNMP_SOCKET;
	must_write(PRIV_UNPRIVILEGED, &cmd, sizeof(enum priv_cmd));
	must_write(PRIV_UNPRIVILEGED, addr, sizeof(struct sockaddr_un));
	priv_wait();
	must_read(PRIV_UNPRIVILEGED, &rc, sizeof(int));
	if (rc < 0)
		return rc;
	return receive_fd(PRIV_UNPRIVILEGED);
}

static void
asroot_ping()
{
	int rc = 1;
	must_write(PRIV_PRIVILEGED, &rc, sizeof(int));
}

static void
asroot_ctl_cleanup()
{
	int len;
	char *ctlname;
	int rc = 0;

	must_read(PRIV_PRIVILEGED, &len, sizeof(int));
	if ((ctlname = (char*)malloc(len+1)) == NULL)
		fatal("ctlname", NULL);

	must_read(PRIV_PRIVILEGED, ctlname, len);
	ctlname[len] = 0;

	ctl_cleanup(ctlname);
	free(ctlname);

	/* Ack */
	must_write(PRIV_PRIVILEGED, &rc, sizeof(int));
}

static void
asroot_gethostname()
{
	struct utsname un;
	struct addrinfo hints = {
		.ai_flags = AI_CANONNAME
	};
	struct addrinfo *res;
	int len;
	if (uname(&un) < 0)
		fatal("privsep", "failed to get system information");
	if (getaddrinfo(un.nodename, NULL, &hints, &res) != 0) {
		log_info("privsep", "unable to get system name");
#ifdef HAVE_RES_INIT
		res_init();
#endif
                len = strlen(un.nodename);
                must_write(PRIV_PRIVILEGED, &len, sizeof(int));
                must_write(PRIV_PRIVILEGED, un.nodename, len);
        } else {
                len = strlen(res->ai_canonname);
                must_write(PRIV_PRIVILEGED, &len, sizeof(int));
                must_write(PRIV_PRIVILEGED, res->ai_canonname, len);
		freeaddrinfo(res);
        }
}

static void
asroot_iface_init()
{
	int rc = -1, fd = -1;
	int ifindex;
	char name[IFNAMSIZ];
	must_read(PRIV_PRIVILEGED, &ifindex, sizeof(ifindex));
	must_read(PRIV_PRIVILEGED, &name, sizeof(name));
	name[sizeof(name) - 1] = '\0';

	TRACE(LLDPD_PRIV_INTERFACE_INIT(name));
	rc = asroot_iface_init_os(ifindex, name, &fd);
	must_write(PRIV_PRIVILEGED, &rc, sizeof(rc));
	if (rc == 0 && fd >=0) send_fd(PRIV_PRIVILEGED, fd);
	if (fd >= 0) close(fd);
}

static void
asroot_iface_multicast()
{
	int sock = -1, add, rc = 0;
	struct ifreq ifr = { .ifr_name = {} };
	must_read(PRIV_PRIVILEGED, ifr.ifr_name, IFNAMSIZ);
#if defined HOST_OS_LINUX
	must_read(PRIV_PRIVILEGED, ifr.ifr_hwaddr.sa_data, ETHER_ADDR_LEN);
#elif defined HOST_OS_FREEBSD || defined HOST_OS_OSX || defined HOST_OS_DRAGONFLY
	/* Black magic from mtest.c */
	struct sockaddr_dl *dlp = ALIGNED_CAST(struct sockaddr_dl *, &ifr.ifr_addr);
	dlp->sdl_len = sizeof(struct sockaddr_dl);
	dlp->sdl_family = AF_LINK;
	dlp->sdl_index = 0;
	dlp->sdl_nlen = 0;
	dlp->sdl_alen = ETHER_ADDR_LEN;
	dlp->sdl_slen = 0;
	must_read(PRIV_PRIVILEGED, LLADDR(dlp), ETHER_ADDR_LEN);
#elif defined HOST_OS_OPENBSD || defined HOST_OS_NETBSD || defined HOST_OS_SOLARIS
	struct sockaddr *sap = (struct sockaddr *)&ifr.ifr_addr;
#if ! defined HOST_OS_SOLARIS
	sap->sa_len = sizeof(struct sockaddr);
#endif
	sap->sa_family = AF_UNSPEC;
	must_read(PRIV_PRIVILEGED, sap->sa_data, ETHER_ADDR_LEN);
#else
#error Unsupported OS
#endif

	must_read(PRIV_PRIVILEGED, &add, sizeof(int));
	if (((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1) ||
	    ((ioctl(sock, (add)?SIOCADDMULTI:SIOCDELMULTI,
		    &ifr) < 0) && (errno != EADDRINUSE)))
		rc = errno;

	if (sock != -1) close(sock);
	must_write(PRIV_PRIVILEGED, &rc, sizeof(rc));
}

static void
asroot_iface_description()
{
	char name[IFNAMSIZ];
	char *description;
	int len, rc;
	must_read(PRIV_PRIVILEGED, &name, sizeof(name));
	name[sizeof(name) - 1] = '\0';
	must_read(PRIV_PRIVILEGED, &len, sizeof(int));
	if ((description = (char*)malloc(len+1)) == NULL)
		fatal("description", NULL);

	must_read(PRIV_PRIVILEGED, description, len);
	description[len] = 0;
	TRACE(LLDPD_PRIV_INTERFACE_DESCRIPTION(name, description));
	rc = asroot_iface_description_os(name, description);
	must_write(PRIV_PRIVILEGED, &rc, sizeof(rc));
	free(description);
}

static void
asroot_iface_promisc()
{
	char name[IFNAMSIZ];
	int rc;
	must_read(PRIV_PRIVILEGED, &name, sizeof(name));
	name[sizeof(name) - 1] = '\0';
	rc = asroot_iface_promisc_os(name);
	must_write(PRIV_PRIVILEGED, &rc, sizeof(rc));
}

static void
asroot_snmp_socket()
{
	int sock, rc;
	static struct sockaddr_un *addr = NULL;
	struct sockaddr_un bogus;

	if (!addr) {
		addr = (struct sockaddr_un *)malloc(sizeof(struct sockaddr_un));
		must_read(PRIV_PRIVILEGED, addr, sizeof(struct sockaddr_un));
	} else
		/* We have already been asked to connect to a socket. We will
		 * connect to the same socket. */
		must_read(PRIV_PRIVILEGED, &bogus, sizeof(struct sockaddr_un));
	if (addr->sun_family != AF_UNIX)
		fatal("privsep", "someone is trying to trick me");
	addr->sun_path[sizeof(addr->sun_path)-1] = '\0';

	if ((sock = socket(PF_UNIX, SOCK_STREAM, 0)) < 0) {
		log_warn("privsep", "cannot open socket");
		must_write(PRIV_PRIVILEGED, &sock, sizeof(int));
		return;
	}
        if ((rc = connect(sock, (struct sockaddr *) addr,
		    sizeof(struct sockaddr_un))) != 0) {
		log_info("privsep", "cannot connect to %s: %s",
                          addr->sun_path, strerror(errno));
		close(sock);
		rc = -1;
		must_write(PRIV_PRIVILEGED, &rc, sizeof(int));
		return;
        }

	int flags;
	if ((flags = fcntl(sock, F_GETFL, NULL)) < 0 ||
	    fcntl(sock, F_SETFL, flags | O_NONBLOCK) < 0) {
		log_warn("privsep", "cannot set sock %s to non-block : %s",
                          addr->sun_path, strerror(errno));

		close(sock);
		rc = -1;
		must_write(PRIV_PRIVILEGED, &rc, sizeof(int));
		return;
	}

	must_write(PRIV_PRIVILEGED, &rc, sizeof(int));
	send_fd(PRIV_PRIVILEGED, sock);
	close(sock);
}

struct dispatch_actions {
	enum priv_cmd msg;
	void(*function)(void);
};

static struct dispatch_actions actions[] = {
	{PRIV_PING, asroot_ping},
	{PRIV_DELETE_CTL_SOCKET, asroot_ctl_cleanup},
	{PRIV_GET_HOSTNAME, asroot_gethostname},
#ifdef HOST_OS_LINUX
	{PRIV_OPEN, asroot_open},
#endif
	{PRIV_IFACE_INIT, asroot_iface_init},
	{PRIV_IFACE_MULTICAST, asroot_iface_multicast},
	{PRIV_IFACE_DESCRIPTION, asroot_iface_description},
	{PRIV_IFACE_PROMISC, asroot_iface_promisc},
	{PRIV_SNMP_SOCKET, asroot_snmp_socket},
	{-1, NULL}
};

/* Main loop, run as root */
static void
priv_loop(int privileged, int once)
{
	enum priv_cmd cmd;
	struct dispatch_actions *a;

#ifdef ENABLE_PRIVSEP
	setproctitle("monitor.");
#ifdef USE_SECCOMP
	if (priv_seccomp_init(privileged, monitored) != 0)
	   fatal("privsep", "cannot continue without seccomp setup");
#endif
#endif
	while (!may_read(PRIV_PRIVILEGED, &cmd, sizeof(enum priv_cmd))) {
		log_debug("privsep", "received command %d", cmd);
		for (a = actions; a->function != NULL; a++) {
			if (cmd == a->msg) {
				a->function();
				break;
			}
		}
		if (a->function == NULL)
			fatalx("privsep", "bogus message received");
		if (once) break;
	}
}

/* This function is a NOOP when privilege separation is enabled. In
 * the other case, it should be called when we wait an action from the
 * privileged side. */
void
priv_wait() {
#ifndef ENABLE_PRIVSEP
	/* We have no remote process on the other side. Let's emulate it. */
	priv_loop(0, 1);
#endif
}


#ifdef ENABLE_PRIVSEP
static void
priv_exit_rc_status(int rc, int status) {
	switch (rc) {
	case 0:
		/* kill child */
		kill(monitored, SIGTERM);
		/* we will receive a sigchld in the future */
		return;
	case -1:
		/* child doesn't exist anymore, we consider this is an error to
		 * be here */
		_exit(1);
		break;
	default:
		/* Monitored child has terminated */
		/* Mimic the exit state of the child */
		if (WIFEXITED(status)) {
			/* Normal exit */
			_exit(WEXITSTATUS(status));
		}
		if (WIFSIGNALED(status)) {
			/* Terminated with signal */
			signal(WTERMSIG(status), SIG_DFL);
			raise(WTERMSIG(status));
			_exit(1); /* We consider that not being killed is an error. */
		}
		/* Other cases, consider this as an error. */
		_exit(1);
		break;
	}
}

static void
priv_exit()
{
	int status;
	int rc;
	rc = waitpid(monitored, &status, WNOHANG);
	priv_exit_rc_status(rc, status);
}

/* If priv parent gets a TERM or HUP, pass it through to child instead */
static void
sig_pass_to_chld(int sig)
{
	int oerrno = errno;
	if (monitored != -1)
		kill(monitored, sig);
	errno = oerrno;
}

/* If priv parent gets a SIGCHLD, it will exit if this is the monitored
 * process. Other processes (including lldpcli)) are just reaped without
 * consequences. */
static void
sig_chld(int sig)
{
	int status;
	int rc = waitpid(monitored, &status, WNOHANG);
	if (rc == 0) {
		while ((rc = waitpid(-1, &status, WNOHANG)) > 0) {
			if (rc == monitored) priv_exit_rc_status(rc, status);
		}
		return;
	}
	priv_exit_rc_status(rc, status);
}

/* Initialization */
#define LOCALTIME "/etc/localtime"
static void
priv_setup_chroot(const char *chrootdir)
{
	/* Create chroot if it does not exist */
	if (mkdir(chrootdir, 0755) == -1) {
		if (errno != EEXIST)
			fatal("privsep", "unable to create chroot directory");
	} else {
		log_info("privsep", "created chroot directory %s",
		    chrootdir);
	}

	/* Check if /etc/localtime exists in chroot or outside chroot */
	char path[1024];
	int source = -1, destination = -1;
	if (snprintf(path, sizeof(path),
		"%s" LOCALTIME, chrootdir) >= sizeof(path))
		return;
	if ((source = open(LOCALTIME, O_RDONLY)) == -1) {
		if (errno == ENOENT)
			return;
		log_warn("privsep", "cannot read " LOCALTIME);
		return;
	}

	/* Prepare copy of /etc/localtime */
	path[strlen(chrootdir) + 4] = '\0';
	if (mkdir(path, 0755) == -1) {
		if (errno != EEXIST) {
			log_warn("privsep", "unable to create %s directory",
			    path);
			close(source);
			return;
		}
	}
	path[strlen(chrootdir) + 4] = '/';

	/* Do copy */
	char buffer[1024];
	ssize_t n;
	mode_t old = umask(S_IWGRP | S_IWOTH);
	if ((destination = open(path,
		    O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0666)) == -1) {
		if (errno != EEXIST)
			log_warn("privsep", "cannot create %s", path);
		close(source);
		umask(old);
		return;
	}
	umask(old);
	while ((n = read(source, buffer, sizeof(buffer))) > 0) {
		ssize_t nw, left = n;
		char *p = buffer;
		while (left > 0) {
			if ((nw = write(destination, p, left)) == -1) {
				if (errno == EINTR) continue;
				log_warn("privsep", "cannot write to %s", path);
				close(source);
				close(destination);
				unlink(path);
				return;
			}
			left -= nw;
			p += nw;
		}
	}
	if (n == -1) {
		log_warn("privsep", "cannot read " LOCALTIME);
		unlink(path);
	} else {
		log_info("privsep", LOCALTIME " copied to chroot");
	}
	close(source);
	close(destination);
}
#else /* !ENABLE_PRIVSEP */

/* Reap any children. It should only be lldpcli since there is not monitored
 * process. */
static void
sig_chld(int sig)
{
	int status = 0;
	while (waitpid(-1, &status, WNOHANG) > 0);
}

#endif

void
priv_init(const char *chrootdir, int ctl, uid_t uid, gid_t gid)
{

	int pair[2];

	/* Create socket pair */
	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) < 0) {
		fatal("privsep",
		    "unable to create socket pair for privilege separation");
	}

	priv_unprivileged_fd(pair[0]);
	priv_privileged_fd(pair[1]);

#ifdef ENABLE_PRIVSEP
	gid_t gidset[1];
	/* Spawn off monitor */
	if ((monitored = fork()) < 0)
		fatal("privsep", "unable to fork monitor");
	switch (monitored) {
	case 0:
		/* We are in the children, drop privileges */
		if (RUNNING_ON_VALGRIND)
			log_warnx("privsep", "running on valgrind, keep privileges");
		else {
			priv_setup_chroot(chrootdir);
			if (chroot(chrootdir) == -1)
				fatal("privsep", "unable to chroot");
			if (chdir("/") != 0)
				fatal("privsep", "unable to chdir");
			gidset[0] = gid;
#ifdef HAVE_SETRESGID
			if (setresgid(gid, gid, gid) == -1)
				fatal("privsep", "setresgid() failed");
#else
			if (setregid(gid, gid) == -1)
				fatal("privsep", "setregid() failed");
#endif
			if (setgroups(1, gidset) == -1)
				fatal("privsep", "setgroups() failed");
#ifdef HAVE_SETRESUID
			if (setresuid(uid, uid, uid) == -1)
				fatal("privsep", "setresuid() failed");
#else
			if (setreuid(uid, uid) == -1)
				fatal("privsep", "setreuid() failed");
#endif
		}
		close(pair[1]);
		priv_ping();
		break;
	default:
		/* We are in the monitor */
		if (ctl != -1) close(ctl);
		close(pair[0]);
		if (atexit(priv_exit) != 0)
			fatal("privsep", "unable to set exit function");

		/* Install signal handlers */
		const struct sigaction pass_to_child = {
			.sa_handler = sig_pass_to_chld,
			.sa_flags = SA_RESTART
		};
		sigaction(SIGALRM, &pass_to_child, NULL);
		sigaction(SIGTERM, &pass_to_child, NULL);
		sigaction(SIGHUP,  &pass_to_child, NULL);
		sigaction(SIGINT,  &pass_to_child, NULL);
		sigaction(SIGQUIT, &pass_to_child, NULL);
		const struct sigaction child = {
			.sa_handler = sig_chld,
			.sa_flags = SA_RESTART
		};
		sigaction(SIGCHLD, &child, NULL);
		sig_chld(0);	/* Reap already dead children */
		priv_loop(pair[1], 0);
		exit(0);
	}
#else
	const struct sigaction child = {
		.sa_handler = sig_chld,
		.sa_flags = SA_RESTART
	};
	sigaction(SIGCHLD, &child, NULL);
	sig_chld(0);	/* Reap already dead children */
	log_warnx("priv", "no privilege separation available");
	priv_ping();
#endif
}
