/* SPDX-License-Identifier: BSD-2-Clause */
/*
 * dhcpsd - DHCP server daemon
 * Copyright (c) 2025-2026 Roy Marples <roy@marples.name>
 * All rights reserved

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/types.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>

#include <net/if.h>

#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <ifaddrs.h>
#include <pwd.h>
#include <signal.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "bpf.h"
#include "common.h"
#include "config.h"
#include "defs.h"
#include "dhcp.h"
#include "dhcpsd.h"
#include "eloop.h"
#include "if.h"
#include "logerr.h"
#include "plugin.h"
#include "priv.h"
#include "queue.h"
#include "service.h"
#include "unpriv.h"

#ifdef HAVE_CASPER
#include <sys/capsicum.h>

#include <capsicum_helpers.h>
#include <casper/cap_net.h>
#include <libcasper.h>
#endif

#ifdef HAVE_SECCOMP
#include "seccomp.h"
#endif

const int signals[] = { SIGINT, SIGTERM };
const size_t signals_len = ARRAYCOUNT(signals);

static int
dhcpsd_store_leases(struct ctx *ctx)
{
	struct plugin *p;
	int result = 0;

	PLUGIN_FOREACH(ctx, p)
	{
		if (p->p_store_leases != NULL && p->p_store_leases(p) == -1)
			result = -1;
	}
	return result;
}

static void
dhcpsd_signal_cb(int sig, void *arg)
{
	struct ctx *ctx = arg;
	int exit_code = EXIT_FAILURE;

#define SIGMSG "received %s, %s"
	switch (sig) {
	case SIGINT:
		if (ctx->ctx_options & DHCPSD_MAIN)
			loginfox(SIGMSG, "SIGINT", "stopping");
		break;
	case SIGTERM:
		if (ctx->ctx_options & DHCPSD_MAIN)
			loginfox(SIGMSG, "SIGTERM", "stopping");
		exit_code = EXIT_SUCCESS;
		break;
	default:
		logerrx("received signal %d, no idea what to do", sig);
		return;
	}

	ctx->ctx_options |= DHCPSD_EXITING;
	eloop_exit(ctx->ctx_eloop, exit_code);
}

int
dhcpsd_dropperms(int do_chroot)
{
#ifdef ASAN
	logwarnx("not dropping permissions due to ASAN");
	UNUSED(do_chroot);
#else
	struct passwd *pw;

	pw = getpwnam(DHCPSD_USER);
	if (pw == NULL) {
		logerrx("%s: no such user %s", __func__, DHCPSD_USER);
		return -1;
	}

#if !defined(HAVE_CASPER) && !defined(HAVE_PLEDGE)
	if (do_chroot && chroot(pw->pw_dir) == -1) {
		logerr("%s: chroot: %s", __func__, pw->pw_dir);
		return -1;
	}
#else
	UNUSED(do_chroot);
#endif

/* Avoid a compile warnings on Darwin. */
#ifdef __APPLE__
#define INITGROUPS_GID(gid) (int)(gid)
#else
#define INITGROUPS_GID(gid) (gid)
#endif

	if (initgroups(DHCPSD_USER, INITGROUPS_GID(pw->pw_gid)) == -1 ||
	    setgid(pw->pw_gid) == -1 || setuid(pw->pw_uid) == -1) {
		logerr("%s: error dropping privileges", __func__);
		return -1;
	}
#endif

	return 0;
}

static int
dhcpsd_mkdbdir(void)
{
	struct passwd *pw;

	pw = getpwnam(DHCPSD_USER);

	if (mkdir(DBDIR, 0770) == 0) {
		if (chown(DBDIR, pw->pw_uid, pw->pw_gid) == -1) {
			logerr("%s: chown: %s", __func__, DBDIR);
			return -1;
		}
	} else if (errno != EEXIST) {
		logerr("%s: mkdir: %s", __func__, DBDIR);
		return -1;
	}

	return 0;
}

static void
dhcpsd_fork_cb(void *arg, unsigned short e)
{
	struct ctx *ctx = arg;
	int exit_code;
	ssize_t nread;

	if (e & ELE_HANGUP) {
	hangup:
		close(ctx->ctx_fork_fd);
		ctx->ctx_fork_fd = -1;
		eloop_exit(ctx->ctx_eloop, EXIT_FAILURE);
		return;
	}
	nread = recv(ctx->ctx_fork_fd, &exit_code, sizeof(exit_code), 0);
	if (nread == 0)
		goto hangup;
	if (nread == -1) {
		logerr("%s: recv %d", __func__, ctx->ctx_fork_fd);
		goto hangup;
	}
	if ((size_t)nread != sizeof(exit_code)) {
		logerrx("%s: truncated %zd", __func__, nread);
		goto hangup;
	}
	eloop_exit(ctx->ctx_eloop, exit_code);
}

/* Complicated, but it ensures we don't get a controlling terminal
 * and the ppid of any child processed match the main process. */
static pid_t
dhcpsd_fork(struct ctx *ctx)
{
	int fork_fd[2];
	pid_t pid;
#ifdef HAVE_CASPERx
	cap_rights_t rights;
#endif

	if (xsocketpair(PF_LOCAL, SOCK_STREAM | SOCK_CLOEXEC, 0, fork_fd) ==
	    -1) {
		logerr("socketpair");
		return -1;
	}

	switch (pid = fork()) {
	case -1:
		logerr("%s: fork", __func__);
		return -1;
		;
	case 0:
		ctx->ctx_fork_fd = fork_fd[1];
		close(fork_fd[0]);
#ifdef HAVE_CASPERx
		/* XXX This is failing on FreeBSD-15 */
		cap_rights_init(&rights, CAP_WRITE);
		if (caph_rights_limit(ctx->ctx_fork_fd, &rights) == -1) {
			logerr("%s: caph_rights_limit", __func__);
			return -1;
		}
#endif
		if (setsid() == -1) {
			logerr("%s: setsid", __func__);
			return -1;
		}
		/* Ensure we can never get a controlling terminal */
		switch (pid = fork()) {
		case -1:
			logerr("%s: fork", __func__);
			return -1;
		case 0:
			/* setsid again to ensure our child processes have the
			 * correct ppid */
			if (setsid() == -1) {
				logerr("%s: setsid", __func__);
				return -1;
			}
			if (eloop_forked(ctx->ctx_eloop, ELF_KEEP_ALL) == -1) {
				logerr("%s: eloop_forked", __func__);
				return -1;
			}
			if (eloop_event_add(ctx->ctx_eloop, ctx->ctx_fork_fd,
				ELE_READ, dhcpsd_fork_cb, ctx) == -1) {
				logerr("%s: eloop_event_add", __func__);
				return -1;
			}
			break;
		default:
			ctx->ctx_options &= ~DHCPSD_MAIN;
			break;
		}
		break;
	default:
#ifdef HAVE_SETPROCTITLE
		setproctitle("[launcher]");
#endif
		ctx->ctx_options &= ~DHCPSD_MAIN;
		ctx->ctx_options |= DHCPSD_LAUNCHER;
		ctx->ctx_fork_fd = fork_fd[0];
		close(fork_fd[1]);

#ifdef HAVE_CASPERx
		cap_rights_init(&rights, CAP_READ);
		if (caph_rights_limit(ctx->ctx_fork_fd, &rights) == -1) {
			logerr("%s: caph_rights_limit", __func__);
			return -1;
		}
#endif
		if (eloop_event_add(ctx->ctx_eloop, ctx->ctx_fork_fd, ELE_READ,
			dhcpsd_fork_cb, ctx) == -1) {
			logerr("%s: eloop_event_add", __func__);
			return -1;
		}
		break;
	}
	return pid;
}

static void
dhcpsd_send_launcher(struct ctx *ctx, int exit_code)
{
	if (ctx->ctx_fork_fd == -1)
		return;
	if (send(ctx->ctx_fork_fd, &exit_code, sizeof(exit_code), 0) == -1)
		logerr("%s: send", __func__);
	close(ctx->ctx_fork_fd);
	ctx->ctx_fork_fd = -1;
}

ssize_t
dhcpsd_configure_pools(struct interface *ifp)
{
	struct ctx *ctx = ifp->if_ctx;
	ssize_t npools = 0;
	struct plugin *p;

	if (!(ifp->if_flags & IF_ACTIVE))
		return 0;

	PLUGIN_FOREACH(ctx, p)
	{
		if (p->p_configure_pools == NULL)
			continue;
		ssize_t n = p->p_configure_pools(p, ifp);
		if (n == -1 && ctx->ctx_argc != 0)
			return -1;
		if (n == -1 || n == 0)
			continue;
		/* XXX When we grow DHCPv6 only open BPF if we configure
		 * a DHCPv4 pool. */
		if (ctx->ctx_options & DHCPSD_WAITIF) {
			if (priv_openbpf(ifp) == -1)
				return -1;
		} else {
			ifp->if_bpf = bpf_open(ifp, bpf_bootp, O_WRONLY);
			if (ifp->if_bpf == NULL)
				return -1;
		}
		/* First plugin with config wins */
		npools += n;
		break;
	}

	return npools;
}

int
main(int argc, char **argv)
{
	struct if_head ifaces;
	struct ctx ctx = {
		.ctx_options = DHCPSD_MAIN,
		.ctx_ifaces = &ifaces,
		.ctx_fork_fd = -1,
		.ctx_pf_inet_fd = -1,
#ifdef IFLR_ACTIVE
		.ctx_pf_link_fd = -1,
#endif
	};
	int ch, exit_code = EXIT_FAILURE;
	bool f_flag = false;
	size_t npools;
	struct interface *ifp;
	unsigned int logopts = LOGERR_LOG;
	struct plugin *p;
	struct rlimit rzero = { .rlim_cur = 0, .rlim_max = 0 };
#ifdef HAVE_CASPER
	cap_channel_t *capcas;
	cap_net_limit_t *limit;
#endif

	TAILQ_INIT(ctx.ctx_ifaces);
	closefrom(STDERR_FILENO + 1);

	logopts |= LOGERR_ERR; // log to stderr
#define OPTS "dfp:w"
	while ((ch = getopt(argc, argv, OPTS)) != -1) {
		switch (ch) {
		case 'd':
			logopts |= LOGERR_DEBUG;
			break;
		case 'f':
			f_flag = true;
			logopts |= LOGERR_ERR;	// log to stderr
			logopts &= ~LOGERR_LOG; // don't syslog
			break;
		case 'w':
			ctx.ctx_options |= DHCPSD_WAITIF;
			break;
		case '?':
			goto exit;
		}
	}

	logsetopts(logopts);
	logopen(NULL);

	ctx.ctx_eloop = eloop_new();
	if (ctx.ctx_eloop == NULL) {
		logerr("%s: eloop_new", __func__);
		goto exit;
	}
	if (eloop_signal_set_cb(ctx.ctx_eloop, signals, signals_len,
		dhcpsd_signal_cb, &ctx) == -1) {
		logerr("%s: eloop_signal_set_cb", __func__);
		goto exit;
	}

	if (!(f_flag) && dhcpsd_fork(&ctx) == -1)
		goto exit;
	if (ctx.ctx_options & DHCPSD_LAUNCHER)
		goto run;
	if (!(ctx.ctx_options & DHCPSD_MAIN))
		goto exit;

	loginfox(PACKAGE "-" VERSION " starting");
	if (dhcpsd_mkdbdir() == -1)
		goto exit;

	if (eloop_signal_mask(ctx.ctx_eloop) == -1) {
		logerr("%s: eloop_signal_mask", __func__);
		goto exit;
	}

	optind = 1;
	while ((ch = getopt(argc, argv, OPTS)) != -1) {
		switch (ch) {
		case 'p':
			if (plugin_load(&ctx, optarg) == -1)
				goto exit;
			if (ctx.ctx_options & DHCPSD_RUN)
				goto run;
			break;
		}
	}

	argc -= optind;
	argv += optind;
	ctx.ctx_argc = argc;
	ctx.ctx_argv = argv;

	/* Waiting for interfaces requires a privileged helper
	 * to open BPF sockets. */
	if (ctx.ctx_options & DHCPSD_WAITIF) {
		if (priv_init(&ctx) == NULL) {
			logerr("%s: priv_init", __func__);
			goto exit;
		}
		if (ctx.ctx_options & DHCPSD_RUN)
			goto open_pf_inet;
	}

	if (unpriv_init(&ctx) == NULL) {
		logerr("%s: unpriv_init", __func__);
		goto exit;
	}

	if (ctx.ctx_plugins == NULL) {
		loginfox("no plugins specified");
		if (plugin_load(&ctx, "auto") == -1)
			goto exit;
		if (plugin_load(&ctx, "leasefile") == -1)
			goto exit;
	}

	PLUGIN_FOREACH(&ctx, p)
	{
		if (ctx.ctx_options & DHCPSD_PRIV ||
		    (ctx.ctx_options & DHCPSD_UNPRIV && !p->p_unpriv)) {
			plugin_unload(p);
			continue;
		}
		if (p->p_init != NULL) {
			ch = p->p_init(p);
			if (ch == -1)
				goto exit;
			if (ch != 0)
				goto run;
		}
	}

open_pf_inet:
	ctx.ctx_pf_inet_fd = xsocket(PF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0);
	if (ctx.ctx_pf_inet_fd == -1) {
		logerr("%s: PF_INET", __func__);
		goto exit;
	}

#ifdef IFLR_ACTIVE
	ctx.ctx_pf_link_fd = xsocket(PF_LINK, SOCK_DGRAM | SOCK_CLOEXEC, 0);
	if (ctx.ctx_pf_link_fd == -1) {
		logerr("%s: PF_LINK", __func__);
		goto exit;
	}
#endif

	if (ctx.ctx_options & DHCPSD_RUN)
		goto run;

	if (link_open(&ctx) == -1) {
		logerr("%s: link_open", __func__);
		goto exit;
	}

	if (!(ctx.ctx_options & DHCPSD_WAITIF)) {
		if_learnifaces(&ctx);
		TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) {
			if (argc == 0)
				ifp->if_flags |= IF_ACTIVE;
			else {
				for (ch = 0; ch < argc; ch++) {
					if (strcmp(argv[ch], ifp->if_name) ==
					    0) {
						ifp->if_flags |= IF_ACTIVE;
						argv[ch][0] = '\0';
						break;
					}
				}
			}
		}

		for (ch = 0; ch < argc; ch++) {
			if (argv[ch][0] != '\0') {
				logerrx("%s: no such interface", argv[ch]);
				goto exit;
			}
		}
	}

	ctx.ctx_dhcp = dhcp_new(&ctx);
	if (ctx.ctx_dhcp == NULL)
		goto exit;

	PLUGIN_FOREACH(&ctx, p)
	{
		if (p->p_init_proto != NULL && p->p_init_proto(p) == -1)
			goto exit;
	}

	npools = 0;
	TAILQ_FOREACH(ifp, ctx.ctx_ifaces, if_next) {
		if (!(ifp->if_flags & IF_ACTIVE))
			continue;
		ssize_t n = dhcpsd_configure_pools(ifp);
		if (n == -1 && argc != 0)
			goto exit;
		if (n == -1 || n == 0)
			continue;
		npools += (size_t)n;
	}
	if (!(ctx.ctx_options & DHCPSD_WAITIF) && npools == 0) {
		logerrx("no pools, nothing to serve");
		goto exit;
	}

	loginfox("dropping to user: %s", DHCPSD_USER);
	if (dhcpsd_dropperms(1) == -1)
		goto exit;

#ifdef HAVE_SETPROCTITLE
	setproctitle("DHCP Server Daemon");
#endif

#ifdef HAVE_CASPER
	logdebugx("enabling capsicum");

	caph_cache_catpages();

	capcas = cap_init();
	if (capcas == NULL) {
		logerr("%s: cap_init", __func__);
		goto exit;
	}

	if (caph_enter_casper() == -1) {
		logerr("%s: caph_enter_casper", __func__);
		goto exit;
	}

	ctx.ctx_capnet = cap_service_open(capcas, "system.net");
	if (ctx.ctx_capnet == NULL) {
		logerr("%s: cap_service_open: system.net", __func__);
		goto exit;
	}

	cap_close(capcas);

	limit = cap_net_limit_init(ctx.ctx_capnet,
	    CAPNET_CONNECT | CAPNET_NAME2ADDR);
	if (limit == NULL) {
		logerr("%s: cap_net_limit_init", __func__);
		goto exit;
	}
	if (cap_net_limit(limit) == -1) {
		logerr("%s: cap_net_limit", __func__);
		goto exit;
	}

	caph_limit_stdout();
	caph_limit_stderr();
#endif

/* Prohibit new files, sockets, etc */
/*
 * If poll(2) is called with nfds>RLIMIT_NOFILE then it returns EINVAL.
 * We don't know the final value of nfds at this point *easily*.
 * Sadly, this is a POSIX limitation and most platforms adhere to it.
 * However, some are not that strict and are whitelisted below.
 * Also, if we're not using poll then we can be restrictive.
 *
 * For the non whitelisted platforms there should be a sandbox to
 * fallback to where we don't allow new files, etc:
 *      Linux:seccomp, FreeBSD:capsicum, OpenBSD:pledge
 * Solaris users are sadly out of luck on both counts.
 */
#if defined(__DragonFly__) || defined(__NetBSD__)
	if (setrlimit(RLIMIT_NOFILE, &rzero) == -1)
		logerr("setrlimit RLIMIT_NOFILE");
#endif

/* Prohibit forks */
#ifdef RLIMIT_NPROC
	if (setrlimit(RLIMIT_NPROC, &rzero) == -1)
		logerr("setrlimit RLIMIT_NPROC");
#endif

#ifdef HAVE_PLEDGE
/*
 * stdio is just needed.
 * inet for sendto(2) to work with a non NULL to address.
 */
#define PLEDGE "stdio inet"
	logdebugx("pledge: " PLEDGE);
	if (pledge(PLEDGE, NULL) == -1) {
		logerr("%s: pledge", __func__);
		goto exit;
	}
#endif

#ifdef HAVE_SECCOMP
	logdebugx("enabling SECCOMP");
	if (seccomp_enter() == -1) {
		logerr("%s: seccomp_enter", __func__);
		goto exit;
	}
#endif

	loginfox(PACKAGE " is now running");
	if (ctx.ctx_fork_fd != -1) {
		dhcpsd_send_launcher(&ctx, EXIT_SUCCESS);
		logopts &= ~LOGERR_ERR; // stop logging to stderr
		logsetopts(logopts);
	}
	dhcp_expire_leases(ctx.ctx_dhcp);

run:
	exit_code = eloop_start(ctx.ctx_eloop);
	if (exit_code < 0) {
		logerr("%s: eloop_start", __func__);
		exit_code = EXIT_FAILURE;
	}

	if (dhcpsd_store_leases(&ctx) == -1)
		exit_code = EXIT_FAILURE;

exit:
	plugin_unloadall(&ctx);
	while ((ifp = TAILQ_FIRST(ctx.ctx_ifaces)) != NULL) {
		TAILQ_REMOVE(ctx.ctx_ifaces, ifp, if_next);
		if_free(ifp);
	}
	dhcp_free(ctx.ctx_dhcp);
	eloop_free(ctx.ctx_eloop);
	ctx.ctx_eloop = NULL;
	srv_free(ctx.ctx_unpriv);
	srv_free(ctx.ctx_priv);
	link_free(&ctx);
#ifdef HAVE_CASPER
	if (ctx.ctx_capnet)
		cap_close(ctx.ctx_capnet);
#endif
	if (ctx.ctx_options & DHCPSD_MAIN) {
		loginfox("dhcpsd exited");
		dhcpsd_send_launcher(&ctx, exit_code);
	}

	return exit_code;
}
