/*
 * (C) 2014 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.
 */
#include "config.h"
#include <time.h>
#include "xtables-multi.h"
#include "nft.h"

#include <string.h>
#include <netdb.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdarg.h>
#include <limits.h>
#include <unistd.h>
#include <iptables.h>
#include <xtables.h>
#include <libiptc/libxtc.h>
#include <fcntl.h>
#include <getopt.h>
#include "xshared.h"
#include "nft-shared.h"

void xlate_ifname(struct xt_xlate *xl, const char *nftmeta, const char *ifname,
		  bool invert)
{
	int ifaclen = strlen(ifname), i, j;
	char iface[IFNAMSIZ * 2];

	if (ifaclen < 1 || ifaclen >= IFNAMSIZ)
		return;

	for (i = 0, j = 0; i < ifaclen + 1; i++, j++) {
		switch (ifname[i]) {
		case '*':
			iface[j++] = '\\';
			/* fall through */
		default:
			iface[j] = ifname[i];
			break;
		}
	}

	if (ifaclen == 1 && ifname[0] == '+') {
		/* Nftables does not support wildcard only string. Workaround
		 * is easy, given that this will match always or never
		 * depending on 'invert' value. To match always, simply don't
		 * generate an expression. To match never, use an invalid
		 * interface name (kernel doesn't accept '/' in names) to match
		 * against. */
		if (!invert)
			return;
		strcpy(iface, "INVAL/D");
		invert = false;
	}

	if (iface[j - 2] == '+')
		iface[j - 2] = '*';

	xt_xlate_add(xl, "%s %s\"%s\" ", nftmeta, invert ? "!= " : "", iface);
}

int xlate_action(const struct iptables_command_state *cs, bool goto_set,
		 struct xt_xlate *xl)
{
	int ret = 1, numeric = cs->options & OPT_NUMERIC;

	/* If no target at all, add nothing (default to continue) */
	if (cs->target != NULL) {
		/* Standard target? */
		if (strcmp(cs->jumpto, XTC_LABEL_ACCEPT) == 0)
			xt_xlate_add(xl, " accept");
		else if (strcmp(cs->jumpto, XTC_LABEL_DROP) == 0)
			xt_xlate_add(xl, " drop");
		else if (strcmp(cs->jumpto, XTC_LABEL_RETURN) == 0)
			xt_xlate_add(xl, " return");
		else if (cs->target->xlate) {
			xt_xlate_add(xl, " ");
			struct xt_xlate_tg_params params = {
				.ip		= (const void *)&cs->fw,
				.target		= cs->target->t,
				.numeric	= numeric,
				.escape_quotes	= !cs->restore,
			};
			ret = cs->target->xlate(xl, &params);
		}
		else
			return 0;
	} else if (strlen(cs->jumpto) > 0) {
		/* Not standard, then it's a go / jump to chain */
		if (goto_set)
			xt_xlate_add(xl, " goto %s", cs->jumpto);
		else
			xt_xlate_add(xl, " jump %s", cs->jumpto);
	}

	return ret;
}

int xlate_matches(const struct iptables_command_state *cs, struct xt_xlate *xl)
{
	struct xtables_rule_match *matchp;
	int ret = 1, numeric = cs->options & OPT_NUMERIC;

	for (matchp = cs->matches; matchp; matchp = matchp->next) {
		struct xt_xlate_mt_params params = {
			.ip		= (const void *)&cs->fw,
			.match		= matchp->match->m,
			.numeric	= numeric,
			.escape_quotes	= !cs->restore,
		};

		if (!matchp->match->xlate)
			return 0;

		ret = matchp->match->xlate(xl, &params);

		if (strcmp(matchp->match->name, "comment") != 0)
			xt_xlate_add(xl, " ");

		if (!ret)
			break;
	}
	return ret;
}

bool xlate_find_match(const struct iptables_command_state *cs, const char *p_name)
{
	struct xtables_rule_match *matchp;

	/* Skip redundant protocol, eg. ip protocol tcp tcp dport */
	for (matchp = cs->matches; matchp; matchp = matchp->next) {
		if (strcmp(matchp->match->name, p_name) == 0)
			return true;
	}
	return false;
}

const char *family2str[] = {
	[NFPROTO_IPV4]	= "ip",
	[NFPROTO_IPV6]	= "ip6",
};

static int nft_rule_xlate_add(struct nft_handle *h,
			      const struct nft_xt_cmd_parse *p,
			      const struct iptables_command_state *cs,
			      bool append)
{
	struct xt_xlate *xl = xt_xlate_alloc(10240);
	int ret;

	if (append) {
		xt_xlate_add(xl, "add rule %s %s %s ",
			   family2str[h->family], p->table, p->chain);
	} else {
		xt_xlate_add(xl, "insert rule %s %s %s ",
			   family2str[h->family], p->table, p->chain);
	}

	ret = h->ops->xlate(cs, xl);
	if (ret)
		printf("%s\n", xt_xlate_get(xl));

	xt_xlate_free(xl);
	return ret;
}

static int xlate(struct nft_handle *h, struct nft_xt_cmd_parse *p,
		 struct iptables_command_state *cs,
		 struct xtables_args *args, bool append,
		 int (*cb)(struct nft_handle *h,
			   const struct nft_xt_cmd_parse *p,
			   const struct iptables_command_state *cs,
			   bool append))
{
	unsigned int i, j;
	int ret = 1;

	for (i = 0; i < args->s.naddrs; i++) {
		switch (h->family) {
		case AF_INET:
			cs->fw.ip.src.s_addr = args->s.addr.v4[i].s_addr;
			cs->fw.ip.smsk.s_addr = args->s.mask.v4[i].s_addr;
			for (j = 0; j < args->d.naddrs; j++) {
				cs->fw.ip.dst.s_addr =
					args->d.addr.v4[j].s_addr;
				cs->fw.ip.dmsk.s_addr =
					args->d.mask.v4[j].s_addr;
				ret = cb(h, p, cs, append);
			}
			break;
		case AF_INET6:
			memcpy(&cs->fw6.ipv6.src,
			       &args->s.addr.v6[i], sizeof(struct in6_addr));
			memcpy(&cs->fw6.ipv6.smsk,
			       &args->s.mask.v6[i], sizeof(struct in6_addr));
			for (j = 0; j < args->d.naddrs; j++) {
				memcpy(&cs->fw6.ipv6.dst,
				       &args->d.addr.v6[j],
				       sizeof(struct in6_addr));
				memcpy(&cs->fw6.ipv6.dmsk,
				       &args->d.mask.v6[j],
				       sizeof(struct in6_addr));
				ret = cb(h, p, cs, append);
			}
			break;
		}
		if (!cs->restore && i < args->s.naddrs - 1)
			printf("nft ");
	}

	return ret;
}

static void print_ipt_cmd(int argc, char *argv[])
{
	int i;

	printf("# ");
	for (i = 1; i < argc; i++)
		printf("%s ", argv[i]);

	printf("\n");
}

static int do_command_xlate(struct nft_handle *h, int argc, char *argv[],
			    char **table, bool restore)
{
	int ret = 0;
	struct nft_xt_cmd_parse p = {
		.table		= *table,
		.restore	= restore,
		.xlate		= true,
	};
	struct iptables_command_state cs;
	struct xtables_args args = {
		.family = h->family,
	};

	do_parse(h, argc, argv, &p, &cs, &args);

	cs.restore = restore;

	if (!restore)
		printf("nft ");

	switch (p.command) {
	case CMD_APPEND:
		ret = 1;
		if (!xlate(h, &p, &cs, &args, true, nft_rule_xlate_add))
			print_ipt_cmd(argc, argv);
		break;
	case CMD_DELETE:
		break;
	case CMD_DELETE_NUM:
		break;
	case CMD_CHECK:
		break;
	case CMD_REPLACE:
		break;
	case CMD_INSERT:
		ret = 1;
		if (!xlate(h, &p, &cs, &args, false, nft_rule_xlate_add))
			print_ipt_cmd(argc, argv);
		break;
	case CMD_FLUSH:
		if (p.chain) {
			printf("flush chain %s %s %s\n",
				family2str[h->family], p.table, p.chain);
		} else {
			printf("flush table %s %s\n",
				family2str[h->family], p.table);
		}
		ret = 1;
		break;
	case CMD_ZERO:
		break;
	case CMD_ZERO_NUM:
		break;
	case CMD_LIST:
	case CMD_LIST|CMD_ZERO:
	case CMD_LIST|CMD_ZERO_NUM:
		printf("list table %s %s\n",
		       family2str[h->family], p.table);
		ret = 1;
		break;
	case CMD_LIST_RULES:
	case CMD_LIST_RULES|CMD_ZERO:
	case CMD_LIST_RULES|CMD_ZERO_NUM:
		break;
	case CMD_NEW_CHAIN:
		printf("add chain %s %s %s\n",
		       family2str[h->family], p.table, p.chain);
		ret = 1;
		break;
	case CMD_DELETE_CHAIN:
		printf("delete chain %s %s %s\n",
		       family2str[h->family], p.table, p.chain);
		ret = 1;
		break;
	case CMD_RENAME_CHAIN:
		break;
	case CMD_SET_POLICY:
		break;
	default:
		/* We should never reach this... */
		printf("Unsupported command?\n");
		exit(1);
	}

	xtables_rule_matches_free(&cs.matches);

	if (h->family == AF_INET) {
		free(args.s.addr.v4);
		free(args.s.mask.v4);
		free(args.d.addr.v4);
		free(args.d.mask.v4);
	} else if (h->family == AF_INET6) {
		free(args.s.addr.v6);
		free(args.s.mask.v6);
		free(args.d.addr.v6);
		free(args.d.mask.v6);
	}
	xtables_free_opts(1);

	return ret;
}

static void print_usage(const char *name, const char *version)
{
	fprintf(stderr, "%s %s "
			"(c) 2014 by Pablo Neira Ayuso <pablo@netfilter.org>\n"
			"Usage: %s [-h] [-f]\n"
                        "	[ --help ]\n"
                        "	[ --file=<FILE> ]\n", name, version, name);
        exit(1);
}

static const struct option options[] = {
	{ .name = "help",	.has_arg = false,	.val = 'h' },
	{ .name = "file",	.has_arg = true,	.val = 'f' },
	{ .name = "version",	.has_arg = false,	.val = 'V' },
	{ NULL },
};

static int xlate_chain_user_restore(struct nft_handle *h, const char *chain,
				    const char *table)
{
	printf("add chain %s %s %s\n", family2str[h->family], table, chain);
	return 0;
}

static int commit(struct nft_handle *h)
{
	return 1;
}

static void xlate_table_new(struct nft_handle *h, const char *table)
{
	printf("add table %s %s\n", family2str[h->family], table);
}

static int get_hook_prio(const char *table, const char *chain)
{
	int prio = 0;

	if (strcmp("nat", table) == 0) {
		if (strcmp(chain, "PREROUTING") == 0)
			prio = NF_IP_PRI_NAT_DST;
		if (strcmp(chain, "INPUT") == 0)
			prio = NF_IP_PRI_NAT_SRC;
		if (strcmp(chain, "OUTPUT") == 0)
			prio = NF_IP_PRI_NAT_DST;
		if (strcmp(chain, "POSTROUTING") == 0)
			prio = NF_IP_PRI_NAT_SRC;
	} else if (strcmp("mangle", table) == 0) {
		prio = NF_IP_PRI_MANGLE;
	} else if (strcmp("raw", table) == 0) {
		prio = NF_IP_PRI_RAW;
	} else if (strcmp(chain, "security") == 0) {
		prio = NF_IP_PRI_SECURITY;
	}

	return prio;
}

static int xlate_chain_set(struct nft_handle *h, const char *table,
			   const char *chain, const char *policy,
			   const struct xt_counters *counters)
{
	const char *type = "filter";
	int prio;

	if (strcmp(table, "nat") == 0)
		type = "nat";
	else if (strcmp(table, "mangle") == 0 && strcmp(chain, "OUTPUT") == 0)
		type = "route";

	printf("add chain %s %s %s { type %s ",
	       family2str[h->family], table, chain, type);
	prio = get_hook_prio(table, chain);
	if (strcmp(chain, "PREROUTING") == 0)
		printf("hook prerouting priority %d; ", prio);
	else if (strcmp(chain, "INPUT") == 0)
		printf("hook input priority %d; ", prio);
	else if (strcmp(chain, "FORWARD") == 0)
		printf("hook forward priority %d; ", prio);
	else if (strcmp(chain, "OUTPUT") == 0)
		printf("hook output priority %d; ", prio);
	else if (strcmp(chain, "POSTROUTING") == 0)
		printf("hook postrouting priority %d; ", prio);

	if (strcmp(policy, "ACCEPT") == 0)
		printf("policy accept; ");
	else if (strcmp(policy, "DROP") == 0)
		printf("policy drop; ");

	printf("}\n");
	return 1;
}

static int dummy_compat_rev(const char *name, uint8_t rev, int opt)
{
	/* Avoid querying the kernel - it's not needed when just translating
	 * rules and not even possible when running as unprivileged user.
	 */
	return 1;
}

static const struct nft_xt_restore_cb cb_xlate = {
	.table_new	= xlate_table_new,
	.chain_set	= xlate_chain_set,
	.chain_restore	= xlate_chain_user_restore,
	.do_command	= do_command_xlate,
	.commit		= commit,
	.abort		= commit,
};

static int xtables_xlate_main_common(struct nft_handle *h,
				     int family,
				     const char *progname)
{
	const struct builtin_table *tables;
	int ret;

	xtables_globals.program_name = progname;
	xtables_globals.compat_rev = dummy_compat_rev;
	ret = xtables_init_all(&xtables_globals, family);
	if (ret < 0) {
		fprintf(stderr, "%s/%s Failed to initialize xtables\n",
			xtables_globals.program_name,
			xtables_globals.program_version);
		return 1;
	}
	switch (family) {
	case NFPROTO_IPV4:
	case NFPROTO_IPV6: /* fallthrough: same table */
#if defined(ALL_INCLUSIVE) || defined(NO_SHARED_LIBS)
	init_extensions();
	init_extensions4();
#endif
		tables = xtables_ipv4;
		break;
	case NFPROTO_ARP:
		tables = xtables_arp;
		break;
	case NFPROTO_BRIDGE:
		tables = xtables_bridge;
		break;
	default:
		fprintf(stderr, "Unknown family %d\n", family);
		return 1;
	}

	if (nft_init(h, family, tables) < 0) {
		fprintf(stderr, "%s/%s Failed to initialize nft: %s\n",
				xtables_globals.program_name,
				xtables_globals.program_version,
				strerror(errno));
		return 1;
	}

	return 0;
}

static int xtables_xlate_main(int family, const char *progname, int argc,
			      char *argv[])
{
	int ret;
	char *table = "filter";
	struct nft_handle h = {
		.family = family,
	};

	ret = xtables_xlate_main_common(&h, family, progname);
	if (ret < 0)
		exit(EXIT_FAILURE);

	ret = do_command_xlate(&h, argc, argv, &table, false);
	if (!ret)
		fprintf(stderr, "Translation not implemented\n");

	nft_fini(&h);
	xtables_fini();
	exit(!ret);
}

static int xtables_restore_xlate_main(int family, const char *progname,
				      int argc, char *argv[])
{
	int ret;
	struct nft_handle h = {
		.family = family,
	};
	const char *file = NULL;
	struct nft_xt_restore_parse p = {
		.cb = &cb_xlate,
	};
	time_t now = time(NULL);
	int c;

	ret = xtables_xlate_main_common(&h, family, progname);
	if (ret < 0)
		exit(EXIT_FAILURE);

	opterr = 0;
	while ((c = getopt_long(argc, argv, "hf:V", options, NULL)) != -1) {
		switch (c) {
		case 'h':
			print_usage(argv[0], PACKAGE_VERSION);
			exit(0);
		case 'f':
			file = optarg;
			break;
		case 'V':
			printf("%s v%s\n", argv[0], PACKAGE_VERSION);
			exit(0);
		}
	}

	if (file == NULL) {
		fprintf(stderr, "ERROR: missing file name\n");
		print_usage(argv[0], PACKAGE_VERSION);
		exit(0);
	}

	p.in = fopen(file, "r");
	if (p.in == NULL) {
		fprintf(stderr, "Cannot open file %s\n", file);
		exit(1);
	}

	printf("# Translated by %s v%s on %s",
	       argv[0], PACKAGE_VERSION, ctime(&now));
	xtables_restore_parse(&h, &p);
	printf("# Completed on %s", ctime(&now));

	nft_fini(&h);
	xtables_fini();
	fclose(p.in);
	exit(0);
}

int xtables_ip4_xlate_main(int argc, char *argv[])
{
	return xtables_xlate_main(NFPROTO_IPV4, "iptables-translate",
				  argc, argv);
}

int xtables_ip6_xlate_main(int argc, char *argv[])
{
	return xtables_xlate_main(NFPROTO_IPV6, "ip6tables-translate",
				  argc, argv);
}

int xtables_ip4_xlate_restore_main(int argc, char *argv[])
{
	return xtables_restore_xlate_main(NFPROTO_IPV4,
					  "iptables-translate-restore",
					  argc, argv);
}

int xtables_ip6_xlate_restore_main(int argc, char *argv[])
{
	return xtables_restore_xlate_main(NFPROTO_IPV6,
					  "ip6tables-translate-restore",
					  argc, argv);
}
