/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * PPPoE Server-mode daemon for use with Solaris PPP 4.0.
 *
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <stropts.h>
#include <wait.h>
#include <sys/resource.h>
#include <netinet/in.h>
#include <net/sppptun.h>
#include <net/pppoe.h>

#include "common.h"
#include "pppoed.h"
#include "logging.h"

static int tunfd;		/* Global connection to tunnel device */

char *myname;			/* Copied from argv[0] for logging */
static int main_argc;		/* Saved for reparse on SIGHUP */
static char **main_argv;	/* Saved for reparse on SIGHUP */

static time_t time_started;	/* Time daemon was started; for debug */
static time_t last_reread;	/* Last time configuration was read. */

/* Various operational statistics. */
static unsigned long input_packets, padi_packets, padr_packets;
static unsigned long output_packets;
static unsigned long sessions_started;

static sigset_t sigmask;	/* Global signal mask */

/*
 * Used for handling errors that occur before we daemonize.
 */
static void
early_error(const char *str)
{
	const char *cp;

	cp = mystrerror(errno);
	if (isatty(2)) {
		(void) fprintf(stderr, "%s: %s: %s\n", myname, str, cp);
	} else {
		reopen_log();
		logerr("%s: %s", str, cp);
	}
	exit(1);
}

/*
 * Open the sppptun driver.
 */
static void
open_tunnel_dev(void)
{
	struct ppptun_peer ptp;

	tunfd = open(tunnam, O_RDWR);
	if (tunfd == -1) {
		early_error(tunnam);
	}

	/*
	 * Tell the device driver that I'm a daemon handling inbound
	 * connections, not a PPP session.
	 */
	(void) memset(&ptp, '\0', sizeof (ptp));
	ptp.ptp_style = PTS_PPPOE;
	ptp.ptp_flags = PTPF_DAEMON;
	(void) memcpy(ptp.ptp_address.pta_pppoe.ptma_mac, ether_bcast,
	    sizeof (ptp.ptp_address.pta_pppoe.ptma_mac));
	if (strioctl(tunfd, PPPTUN_SPEER, &ptp, sizeof (ptp), sizeof (ptp)) <
	    0) {
		myperror("PPPTUN_SPEER");
		exit(1);
	}
}

/*
 * Callback function for fdwalk.  Closes everything but the tunnel
 * file descriptor when becoming daemon.  (Log file must be reopened
 * manually, since syslog file descriptor, if any, is unknown.)
 */
/*ARGSUSED*/
static int
fdcloser(void *arg, int fd)
{
	if (fd != tunfd)
		(void) close(fd);
	return (0);
}

/*
 * Become a daemon.
 */
static void
daemonize(void)
{
	pid_t cpid;

	/*
	 * A little bit of magic here.  By the first fork+setsid, we
	 * disconnect from our current controlling terminal and become
	 * a session group leader.  By forking again without setsid,
	 * we make certain that we're not the session group leader and
	 * can never reacquire a controlling terminal.
	 */
	if ((cpid = fork()) == (pid_t)-1) {
		early_error("fork 1");
	}
	if (cpid != 0) {
		(void) wait(NULL);
		_exit(0);
	}
	if (setsid() == (pid_t)-1) {
		early_error("setsid");
	}
	if ((cpid = fork()) == (pid_t)-1) {
		early_error("fork 2");
	}
	if (cpid != 0) {
		/* Parent just exits */
		(void) printf("%d\n", (int)cpid);
		(void) fflush(stdout);
		_exit(0);
	}
	(void) chdir("/");
	(void) umask(0);
	(void) fdwalk(fdcloser, NULL);
	reopen_log();
}

/*
 * Handle SIGHUP -- close and reopen non-syslog log files and reparse
 * options.
 */
/*ARGSUSED*/
static void
handle_hup(int sig)
{
	close_log_files();
	global_logging();
	last_reread = time(NULL);
	parse_options(tunfd, main_argc, main_argv);
}

/*
 * Handle SIGINT -- write current daemon status to /tmp.
 */
/*ARGSUSED*/
static void
handle_int(int sig)
{
	FILE *fp;
	char dumpname[MAXPATHLEN];
	time_t now;
	struct rusage rusage;

	(void) snprintf(dumpname, sizeof (dumpname), "/tmp/pppoed.%ld",
	    getpid());
	if ((fp = fopen(dumpname, "w+")) == NULL) {
		logerr("%s: %s", dumpname, mystrerror(errno));
		return;
	}
	now = time(NULL);
	(void) fprintf(fp, "pppoed running %s", ctime(&now));
	(void) fprintf(fp, "Started on     %s", ctime(&time_started));
	if (last_reread != 0)
		(void) fprintf(fp, "Last reconfig  %s", ctime(&last_reread));
	(void) putc('\n', fp);
	if (getrusage(RUSAGE_SELF, &rusage) == 0) {
		(void) fprintf(fp,
		    "CPU usage:  user %ld.%06ld, system %ld.%06ld\n",
		    rusage.ru_utime.tv_sec, rusage.ru_utime.tv_usec,
		    rusage.ru_stime.tv_sec, rusage.ru_stime.tv_usec);
	}
	(void) fprintf(fp, "Packets:  %lu received (%lu PADI, %lu PADR), ",
	    input_packets, padi_packets, padr_packets);
	(void) fprintf(fp, "%lu transmitted\n", output_packets);
	(void) fprintf(fp, "Sessions started:  %lu\n\n", sessions_started);
	dump_configuration(fp);
	(void) fclose(fp);
}

static void
add_signal_handlers(void)
{
	struct sigaction sa;

	(void) sigemptyset(&sigmask);
	(void) sigaddset(&sigmask, SIGHUP);
	(void) sigaddset(&sigmask, SIGCHLD);
	(void) sigaddset(&sigmask, SIGINT);
	(void) sigprocmask(SIG_BLOCK, &sigmask, NULL);

	sa.sa_mask = sigmask;
	sa.sa_flags = 0;

	/* Signals to handle */
	sa.sa_handler = handle_hup;
	if (sigaction(SIGHUP, &sa, NULL) < 0)
		early_error("sigaction HUP");
	sa.sa_handler = handle_int;
	if (sigaction(SIGINT, &sa, NULL) < 0)
		early_error("sigaction INT");

	/*
	 * Signals to ignore.  Ignoring SIGCHLD in this way makes the
	 * children exit without ever creating zombies.  (No wait(2)
	 * call required.)
	 */
	sa.sa_handler = SIG_IGN;
	if (sigaction(SIGPIPE, &sa, NULL) < 0)
		early_error("sigaction PIPE");
	sa.sa_flags = SA_NOCLDWAIT;
	if (sigaction(SIGCHLD, &sa, NULL) < 0)
		early_error("sigaction CHLD");
}

/*
 * Dispatch a message from the tunnel driver.  It could be an actual
 * PPPoE message or just an event notification.
 */
static void
handle_input(uint32_t *ctrlbuf, int ctrllen, uint32_t *databuf, int datalen)
{
	poep_t *poep = (poep_t *)databuf;
	union ppptun_name ptn;
	int retv;
	struct strbuf ctrl;
	struct strbuf data;
	void *srvp;
	boolean_t launch;
	struct ppptun_control *ptc;

	if (ctrllen != sizeof (*ptc)) {
		logdbg("bogus %d byte control message from driver",
		    ctrllen);
		return;
	}
	ptc = (struct ppptun_control *)ctrlbuf;

	/* Switch out on event notifications. */
	switch (ptc->ptc_action) {
	case PTCA_TEST:
		logdbg("test reply for discriminator %X", ptc->ptc_discrim);
		return;

	case PTCA_CONTROL:
		break;

	case PTCA_DISCONNECT:
		logdbg("session %d disconnected on %s; send PADT",
		    ptc->ptc_rsessid, ptc->ptc_name);
		poep = poe_mkheader(pkt_output, POECODE_PADT,
		    ptc->ptc_rsessid);
		ptc->ptc_action = PTCA_CONTROL;
		ctrl.len = sizeof (*ptc);
		ctrl.buf = (caddr_t)ptc;
		data.len = poe_length(poep) + sizeof (*poep);
		data.buf = (caddr_t)poep;
		if (putmsg(tunfd, &ctrl, &data, 0) < 0) {
			logerr("putmsg PADT: %s", mystrerror(errno));
		} else {
			output_packets++;
		}
		return;

	case PTCA_UNPLUMB:
		logdbg("%s unplumbed", ptc->ptc_name);
		return;

	case PTCA_BADCTRL:
		logwarn("bad control data on %s for session %u", ptc->ptc_name,
		    ptc->ptc_rsessid);
		return;

	default:
		logdbg("unexpected code %d from driver", ptc->ptc_action);
		return;
	}

	/* Only PPPoE control messages get here. */

	input_packets++;
	if (datalen < sizeof (*poep)) {
		logdbg("incomplete PPPoE message from %s/%s",
		    ehost(&ptc->ptc_address), ptc->ptc_name);
		return;
	}

	/* Server handles only PADI and PADR; all others are ignored. */
	if (poep->poep_code == POECODE_PADI) {
		padi_packets++;
	} else if (poep->poep_code == POECODE_PADR) {
		padr_packets++;
	} else {
		loginfo("unexpected %s from %s",
		    poe_codename(poep->poep_code), ehost(&ptc->ptc_address));
		return;
	}
	logdbg("Recv from %s/%s: %s", ehost(&ptc->ptc_address), ptc->ptc_name,
	    poe_codename(poep->poep_code));

	/* Parse out service and formulate template reply. */
	retv = locate_service(poep, datalen, ptc->ptc_name, &ptc->ptc_address,
	    pkt_output, &srvp);

	/* Continue formulating reply */
	launch = B_FALSE;
	if (retv != 1) {
		/* Ignore initiation if we don't offer a service. */
		if (retv <= 0 && poep->poep_code == POECODE_PADI) {
			logdbg("no services; no reply");
			return;
		}
		if (retv == 0)
			(void) poe_add_str((poep_t *)pkt_output, POETT_NAMERR,
			    "No such service.");
	} else {
		/* Exactly one service chosen; if it's PADR, then we start. */
		if (poep->poep_code == POECODE_PADR) {
			launch = B_TRUE;
		}
	}
	poep = (poep_t *)pkt_output;

	/* Select control interface for output. */
	(void) strncpy(ptn.ptn_name, ptc->ptc_name, sizeof (ptn.ptn_name));
	if (strioctl(tunfd, PPPTUN_SCTL, &ptn, sizeof (ptn), 0) < 0) {
		logerr("PPPTUN_SCTL %s: %s", ptn.ptn_name, mystrerror(errno));
		return;
	}

	/* Launch the PPP service */
	if (launch && launch_service(tunfd, poep, srvp, ptc))
		sessions_started++;

	/* Send the reply. */
	ctrl.len = sizeof (*ptc);
	ctrl.buf = (caddr_t)ptc;
	data.len = poe_length(poep) + sizeof (*poep);
	data.buf = (caddr_t)poep;
	if (putmsg(tunfd, &ctrl, &data, 0) < 0) {
		logerr("putmsg %s: %s", ptc->ptc_name, mystrerror(errno));
	} else {
		output_packets++;
		logdbg("Send to   %s/%s: %s", ehost(&ptc->ptc_address),
		    ptc->ptc_name, poe_codename(poep->poep_code));
	}
}

static void
main_loop(void)
{
	struct strbuf ctrl;
	struct strbuf data;
	int flags;
	int rc;
	int err;

	for (;;) {
		ctrl.maxlen = PKT_OCTL_LEN;
		ctrl.buf = (caddr_t)pkt_octl;
		data.maxlen = PKT_INPUT_LEN;
		data.buf = (caddr_t)pkt_input;
		/* Allow signals only while idle */
		(void) sigprocmask(SIG_UNBLOCK, &sigmask, NULL);
		errno = 0;
		flags = 0;
		rc = mygetmsg(tunfd, &ctrl, &data, &flags);
		err = errno;
		/*
		 * Block signals -- data structures must not change
		 * while we're busy dispatching the client's request
		 */
		(void) sigprocmask(SIG_BLOCK, &sigmask, NULL);
		if (rc == -1) {
			if (err == EAGAIN || err == EINTR)
				continue;
			logerr("%s getmsg: %s", tunnam, mystrerror(err));
			exit(1);
		}
		if (rc > 0)
			logwarn("%s returned truncated data", tunnam);
		else
			handle_input(pkt_octl, ctrl.len, pkt_input, data.len);
	}
}

int
main(int argc, char **argv)
{
	prog_name = "pppoed";
	log_level = 1;		/* Default to error messages only at first */

	time_started = time(NULL);

	if ((myname = argv[0]) == NULL)
		myname = "pppoed";

	main_argc = argc;
	main_argv = argv;

	open_tunnel_dev();
	add_signal_handlers();
	daemonize();

	parse_options(tunfd, argc, argv);
	main_loop();

	return (0);
}