/*
 * (C) 2006-2007 by Pablo Neira Ayuso <pablo@netfilter.org>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Description: run and init functions
 */

#include "conntrackd.h"
#include "netlink.h"
#include "ignore.h"
#include "log.h"
#include "alarm.h"
#include "fds.h"

#include <errno.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

void killer(int foo)
{
	/* no signals while handling signals */
	sigprocmask(SIG_BLOCK, &STATE(block), NULL);

	nfct_close(STATE(event));
	nfct_close(STATE(dump));

	ignore_pool_destroy(STATE(ignore_pool));
	local_server_destroy(&STATE(local));
	STATE(mode)->kill();
	unlink(CONFIG(lockfile));
	dlog(LOG_NOTICE, "---- shutdown received ----");
	close_log();

	sigprocmask(SIG_UNBLOCK, &STATE(block), NULL);

	exit(0);
}

static void child(int foo)
{
	while(wait(NULL) > 0);
}

void local_handler(int fd, void *data)
{
	int ret;
	int type;

	ret = read(fd, &type, sizeof(type));
	if (ret == -1) {
		dlog(LOG_ERR, "can't read from unix socket");
		return;
	}
	if (ret == 0)
		return;

	switch(type) {
	case FLUSH_MASTER:
		dlog(LOG_WARNING, "`conntrackd -F' is deprecated. "
				  "Use conntrack -F instead.");
		if (fork() == 0) {
			execlp("conntrack", "conntrack", "-F", NULL);
			exit(EXIT_SUCCESS);
		}
		return;
	case RESYNC_MASTER:
		dlog(LOG_NOTICE, "resync with master table");
		nl_dump_conntrack_table();
		return;
	}

	if (!STATE(mode)->local(fd, type, data))
		dlog(LOG_WARNING, "unknown local request %d", type);
}

static void do_overrun_alarm(struct alarm_block *a, void *data)
{
	nl_overrun_request_resync();
	add_alarm(&STATE(overrun_alarm), 2, 0);
}

int
init(void)
{
	if (CONFIG(flags) & CTD_STATS_MODE)
		STATE(mode) = &stats_mode;
	else if (CONFIG(flags) & CTD_SYNC_MODE)
		STATE(mode) = &sync_mode;
	else {
		fprintf(stderr, "WARNING: No running mode specified. "
				"Defaulting to statistics mode.\n");
		CONFIG(flags) |= CTD_STATS_MODE;
		STATE(mode) = &stats_mode;
	}

	/* Initialization */
	if (STATE(mode)->init() == -1) {
		dlog(LOG_ERR, "initialization failed");
		return -1;
	}

	/* local UNIX socket */
	if (local_server_create(&STATE(local), &CONFIG(local)) == -1) {
		dlog(LOG_ERR, "can't open unix socket!");
		return -1;
	}

	if (nl_init_event_handler() == -1) {
		dlog(LOG_ERR, "can't open netlink handler: %s",
		     strerror(errno));
		dlog(LOG_ERR, "no ctnetlink kernel support?");
		return -1;
	}

	if (nl_init_dump_handler() == -1) {
		dlog(LOG_ERR, "can't open netlink handler: %s",
		     strerror(errno));
		dlog(LOG_ERR, "no ctnetlink kernel support?");
		return -1;
	}

	if (nl_init_overrun_handler() == -1) {
		dlog(LOG_ERR, "can't open netlink handler: %s",
		     strerror(errno));
		dlog(LOG_ERR, "no ctnetlink kernel support?");
		return -1;
	}

	init_alarm(&STATE(overrun_alarm), NULL, do_overrun_alarm);

	STATE(fds) = create_fds();
	if (STATE(fds) == NULL) {
		dlog(LOG_ERR, "can't create file descriptor pool");
		return -1;
	}

	register_fd(STATE(local).fd, STATE(fds));
	register_fd(nfct_fd(STATE(event)), STATE(fds));
	register_fd(nfct_fd(STATE(overrun)), STATE(fds));

	if (STATE(mode)->register_fds &&
	    STATE(mode)->register_fds(STATE(fds)) == -1) {
		dlog(LOG_ERR, "fds registration failed");
		return -1;
	}

	/* Signals handling */
	sigemptyset(&STATE(block));
	sigaddset(&STATE(block), SIGTERM);
	sigaddset(&STATE(block), SIGINT);
	sigaddset(&STATE(block), SIGCHLD);

	if (signal(SIGINT, killer) == SIG_ERR)
		return -1;

	if (signal(SIGTERM, killer) == SIG_ERR)
		return -1;

	/* ignore connection reset by peer */
	if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
		return -1;

	if (signal(SIGCHLD, child) == SIG_ERR)
		return -1;

	dlog(LOG_NOTICE, "initialization completed");

	return 0;
}

static void __run(struct timeval *next_alarm)
{
	int ret;
	fd_set readfds = STATE(fds)->readfds;

	ret = select(STATE(fds)->maxfd + 1, &readfds, NULL, NULL, next_alarm);
	if (ret == -1) {
		/* interrupted syscall, retry */
		if (errno == EINTR)
			return;

		dlog(LOG_WARNING, "select failed: %s", strerror(errno));
		return;
	}

	/* signals are racy */
	sigprocmask(SIG_BLOCK, &STATE(block), NULL);

	/* order received via UNIX socket */
	if (FD_ISSET(STATE(local).fd, &readfds))
		do_local_server_step(&STATE(local), NULL, local_handler);

	/* conntrack event has happened */
	if (FD_ISSET(nfct_fd(STATE(event)), &readfds)) {
		while ((ret = nfct_catch(STATE(event))) != -1);
		if (ret == -1) {
			switch(errno) {
			case ENOBUFS:
				/*
				 * It seems that ctnetlink can't back off,
				 * it's likely that we're losing events.
				 * Solution: duplicate the socket buffer
				 * size and resync with master conntrack table.
				 */
				nl_resize_socket_buffer(STATE(event));
				nl_overrun_request_resync();
				add_alarm(&STATE(overrun_alarm), 2, 0);
				break;
			case ENOENT:
				/*
				 * We received a message from another
				 * netfilter subsystem that we are not
				 * interested in. Just ignore it.
				 */
				break;
			case EAGAIN:
				break;
			default:
				dlog(LOG_WARNING,
				     "event catch says: %s", strerror(errno));
				break;
			}
		}
	}

	if (FD_ISSET(nfct_fd(STATE(overrun)), &readfds)) {
		del_alarm(&STATE(overrun_alarm));
		nfct_catch(STATE(overrun));
		if (STATE(mode)->purge)
			STATE(mode)->purge();
	}

	if (STATE(mode)->run)
		STATE(mode)->run(&readfds);

	sigprocmask(SIG_UNBLOCK, &STATE(block), NULL);
}

void __attribute__((noreturn))
run(void)
{
	struct timeval next_alarm; 
	struct timeval *next = NULL;

	while(1) {
		sigprocmask(SIG_BLOCK, &STATE(block), NULL);
		if (next != NULL && !timerisset(next))
			next = do_alarm_run(&next_alarm);
		else
			next = get_next_alarm_run(&next_alarm);
		sigprocmask(SIG_UNBLOCK, &STATE(block), NULL);

		__run(next);
	}
}