/*
 * firewall3 - 3rd OpenWrt UCI firewall implementation
 *
 *   Copyright (C) 2013 Jo-Philipp Wich <jo@mein.io>
 *
 * 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.
 */

#include <ctype.h>

#include "ipsets.h"


const struct fw3_option fw3_ipset_opts[] = {
	FW3_OPT("enabled",       bool,           ipset,     enabled),
	FW3_OPT("reload_set",	 bool,           ipset,     reload_set),
	FW3_OPT("counters",	 bool,           ipset,     counters),
	FW3_OPT("comment",	 bool,           ipset,     comment),

	FW3_OPT("name",          string,         ipset,     name),
	FW3_OPT("family",        family,         ipset,     family),

	FW3_OPT("storage",       ipset_method,   ipset,     method),
	FW3_LIST("match",        ipset_datatype, ipset,     datatypes),

	FW3_OPT("iprange",       address,        ipset,     iprange),
	FW3_OPT("portrange",     port,           ipset,     portrange),

	FW3_OPT("netmask",       int,            ipset,     netmask),
	FW3_OPT("maxelem",       int,            ipset,     maxelem),
	FW3_OPT("hashsize",      int,            ipset,     hashsize),
	FW3_OPT("timeout",       int,            ipset,     timeout),

	FW3_OPT("external",      string,         ipset,     external),

	FW3_LIST("entry",        setentry,       ipset,     entries),
	FW3_OPT("loadfile",      string,         ipset,     loadfile),

	{ }
};

#define T(m, t1, t2, t3, r, o) \
	{ FW3_IPSET_METHOD_##m, \
	  FW3_IPSET_TYPE_##t1 | (FW3_IPSET_TYPE_##t2 << 8) | (FW3_IPSET_TYPE_##t3 << 16), \
	  r, o }

enum ipset_optflag {
	OPT_IPRANGE   = (1 << 0),
	OPT_PORTRANGE = (1 << 1),
	OPT_NETMASK   = (1 << 2),
	OPT_HASHSIZE  = (1 << 3),
	OPT_MAXELEM   = (1 << 4),
	OPT_FAMILY    = (1 << 5),
};

struct ipset_type {
	enum fw3_ipset_method method;
	uint32_t types;
	uint8_t required;
	uint8_t optional;
};

static struct ipset_type ipset_types[] = {
	T(BITMAP, IP,   UNSPEC, UNSPEC, OPT_IPRANGE, OPT_NETMASK),
	T(BITMAP, IP,   MAC,    UNSPEC, OPT_IPRANGE, 0),
	T(BITMAP, PORT, UNSPEC, UNSPEC, OPT_PORTRANGE, 0),

	T(HASH,   IP,   UNSPEC, UNSPEC, 0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM | OPT_NETMASK),
	T(HASH,   NET,  UNSPEC, UNSPEC, 0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
	T(HASH,   IP,   PORT,   UNSPEC, 0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
	T(HASH,   NET,  PORT,   UNSPEC, 0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
	T(HASH,   IP,   PORT,   IP,     0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),
	T(HASH,   IP,   PORT,   NET,    0,
	  OPT_FAMILY | OPT_HASHSIZE | OPT_MAXELEM),

	T(LIST,   SET,  UNSPEC, UNSPEC, 0, OPT_MAXELEM),
};


static bool
check_types(struct uci_element *e, struct fw3_ipset *ipset)
{
	int i = 0;
	uint32_t typelist = 0;
	struct fw3_ipset_datatype *type;

	list_for_each_entry(type, &ipset->datatypes, list)
	{
		if (i >= 3)
		{
			warn_section("ipset", ipset, e, "must not have more than 3 datatypes assigned");
			return false;
		}

		typelist |= (type->type << (i++ * 8));
	}

	/* find a suitable storage method if none specified */
	if (ipset->method == FW3_IPSET_METHOD_UNSPEC)
	{
		for (i = 0; i < ARRAY_SIZE(ipset_types); i++)
		{
			/* skip type for v6 if it does not support family */
			if (ipset->family != FW3_FAMILY_V4 &&
			    !(ipset_types[i].optional & OPT_FAMILY))
				continue;

			if (ipset_types[i].types == typelist)
			{
				ipset->method = ipset_types[i].method;

				warn_section("ipset", ipset, e, "defines no storage method, assuming '%s'",
					fw3_ipset_method_names[ipset->method]);

				break;
			}
		}
	}

	//typelist |= ipset->method;

	for (i = 0; i < ARRAY_SIZE(ipset_types); i++)
	{
		if (ipset_types[i].method == ipset->method &&
		    ipset_types[i].types == typelist)
		{
			if (!ipset->external)
			{
				if ((ipset_types[i].required & OPT_IPRANGE) &&
					!ipset->iprange.set)
				{
					warn_section("ipset", ipset, e, "requires an ip range");
					return false;
				}

				if ((ipset_types[i].required & OPT_PORTRANGE) &&
				    !ipset->portrange.set)
				{
					warn_section("ipset", ipset, e, "requires a port range");
					return false;
				}

				if (!(ipset_types[i].required & OPT_IPRANGE) &&
				    ipset->iprange.set)
				{
					warn_section("ipset", ipset, e, "iprange ignored");
					ipset->iprange.set = false;
				}

				if (!(ipset_types[i].required & OPT_PORTRANGE) &&
				    ipset->portrange.set)
				{
					warn_section("ipset", ipset, e, "portrange ignored");
					ipset->portrange.set = false;
				}

				if (!(ipset_types[i].optional & OPT_NETMASK) &&
				    ipset->netmask > 0)
				{
					warn_section("ipset", ipset, e, "netmask ignored");
					ipset->netmask = 0;
				}

				if (!(ipset_types[i].optional & OPT_HASHSIZE) &&
				    ipset->hashsize > 0)
				{
					warn_section("ipset", ipset, e, "hashsize ignored");
					ipset->hashsize = 0;
				}

				if (!(ipset_types[i].optional & OPT_MAXELEM) &&
				    ipset->maxelem > 0)
				{
					warn_section("ipset", ipset, e, "maxelem ignored");
					ipset->maxelem = 0;
				}

				if (!(ipset_types[i].optional & OPT_FAMILY) &&
				    ipset->family != FW3_FAMILY_V4)
				{
					warn_section("ipset", ipset, e, "family ignored");
					ipset->family = FW3_FAMILY_V4;
				}
			}

			return true;
		}
	}

	warn_section("ipset", ipset, e, "has an invalid combination of storage method and matches");
	return false;
}

static bool
check_ipset(struct fw3_state *state, struct fw3_ipset *ipset, struct uci_element *e)
{
	if (!ipset->enabled) {
		return false;
	}

	if (ipset->external)
	{
		if (!*ipset->external)
			ipset->external = NULL;
		else if (!ipset->name)
			ipset->name = ipset->external;
	}

	if (!ipset->name || !*ipset->name)
	{
		warn_section("ipset", ipset, e, "ipset must have a name assigned");
	}
	//else if (fw3_lookup_ipset(state, ipset->name) != NULL)
	//{
	//	warn_section("ipset", ipset, e, "has duplicated set name", ipset->name);
	//}
	else if (ipset->family == FW3_FAMILY_ANY)
	{
		warn_section("ipset", ipset, e, "must not have family 'any'");
	}
	else if (ipset->iprange.set && ipset->family != ipset->iprange.family)
	{
		warn_section("ipset", ipset, e, "has iprange of wrong address family");
	}
	else if (list_empty(&ipset->datatypes))
	{
		warn_section("ipset", ipset, e, "has no datatypes assigned");
	}
	else if (check_types(e, ipset))
	{
		return true;
	}

	return false;
}

static struct fw3_ipset *
fw3_alloc_ipset(struct fw3_state *state)
{
	struct fw3_ipset *ipset;

	ipset = calloc(1, sizeof(*ipset));
	if (!ipset)
		return NULL;

	INIT_LIST_HEAD(&ipset->datatypes);
	INIT_LIST_HEAD(&ipset->entries);

	ipset->comment    = false;
	ipset->counters   = false;
	ipset->enabled    = true;
	ipset->family     = FW3_FAMILY_V4;
	ipset->reload_set = false;
	ipset->timeout    = -1; /* no timeout by default */

	list_add_tail(&ipset->list, &state->ipsets);

	return ipset;
}

void
fw3_load_ipsets(struct fw3_state *state, struct uci_package *p,
		struct blob_attr *a)
{
	struct uci_section *s;
	struct uci_element *e;
	struct fw3_ipset *ipset;
	struct blob_attr *entry;
	unsigned rem;

	INIT_LIST_HEAD(&state->ipsets);

	if (state->disable_ipsets)
		return;

	blob_for_each_attr(entry, a, rem)
	{
		const char *type;
		const char *name = "ubus ipset";

		if (!fw3_attr_parse_name_type(entry, &name, &type))
			continue;

		if (strcmp(type, "ipset"))
			continue;

		ipset = fw3_alloc_ipset(state);
		if (!ipset)
			continue;

		if (!fw3_parse_blob_options(ipset, fw3_ipset_opts, entry, name))
		{
			warn_section("ipset", ipset, NULL, "skipped due to invalid options");
			fw3_free_ipset(ipset);
			continue;
		}

		if (!check_ipset(state, ipset, NULL))
			fw3_free_ipset(ipset);
	}

	uci_foreach_element(&p->sections, e)
	{
		s = uci_to_section(e);

		if (strcmp(s->type, "ipset"))
			continue;

		ipset = fw3_alloc_ipset(state);

		if (!ipset)
			continue;

		if (!fw3_parse_options(ipset, fw3_ipset_opts, s))
			warn_elem(e, "has invalid options");

		if (!check_ipset(state, ipset, e))
			fw3_free_ipset(ipset);
	}
}


static void
load_file(struct fw3_ipset *ipset)
{
	FILE *f;
	char line[128];
	char *p;

	if (!ipset->loadfile)
		return;

	info("   * Loading file %s", ipset->loadfile);

	f = fopen(ipset->loadfile, "r");

	if (!f) {
		info("     ! Skipping due to open error: %s", strerror(errno));
		return;
	}

	while (fgets(line, sizeof(line), f)) {
		p = line;
		while (isspace(*p))
			p++;
		if (*p && *p != '#')
			fw3_pr("add %s %s", ipset->name, line);
	}

	fclose(f);
}

static void
create_ipset(struct fw3_ipset *ipset, struct fw3_state *state)
{
	bool first = true;
	struct fw3_setentry *entry;
	struct fw3_ipset_datatype *type;

	info(" * Creating ipset %s", ipset->name);

	first = true;
	fw3_pr("create %s %s", ipset->name, fw3_ipset_method_names[ipset->method]);

	list_for_each_entry(type, &ipset->datatypes, list)
	{
		fw3_pr("%c%s", first ? ':' : ',', fw3_ipset_type_names[type->type]);
		first = false;
	}

	if (ipset->method == FW3_IPSET_METHOD_HASH)
		fw3_pr(" family inet%s", (ipset->family == FW3_FAMILY_V4) ? "" : "6");

	if (ipset->iprange.set)
	{
		fw3_pr(" range %s", fw3_address_to_string(&ipset->iprange, false, true));
	}
	else if (ipset->portrange.set)
	{
		fw3_pr(" range %u-%u",
		       ipset->portrange.port_min, ipset->portrange.port_max);
	}

	if (ipset->timeout >= 0)
		fw3_pr(" timeout %u", ipset->timeout);

	if (ipset->maxelem > 0)
		fw3_pr(" maxelem %u", ipset->maxelem);

	if (ipset->netmask > 0)
		fw3_pr(" netmask %u", ipset->netmask);

	if (ipset->hashsize > 0)
		fw3_pr(" hashsize %u", ipset->hashsize);

	if (ipset->counters)
		fw3_pr(" counters");

	if (ipset->comment)
		fw3_pr(" comment");

	fw3_pr("\n");

	list_for_each_entry(entry, &ipset->entries, list)
		fw3_pr("add %s %s\n", ipset->name, entry->value);

	load_file(ipset);
}

void
fw3_create_ipsets(struct fw3_state *state, enum fw3_family family,
		  bool reload_set)
{
	unsigned int delay, tries;
	bool exec = false;
	struct fw3_ipset *ipset;

	if (state->disable_ipsets)
		return;

	/* spawn ipsets */
	list_for_each_entry(ipset, &state->ipsets, list)
	{
		if (ipset->family != family)
			continue;

		if (ipset->external)
			continue;

		if (fw3_check_ipset(ipset) &&
		    (reload_set && !ipset->reload_set))
			continue;

		if (!exec)
		{
			exec = fw3_command_pipe(false, "ipset", "-exist", "-");

			if (!exec)
				return;
		}

		create_ipset(ipset, state);
	}

	if (exec)
	{
		fw3_pr("quit\n");
		fw3_command_close();
	}

	/* wait a little expontially for ipsets to appear */
	list_for_each_entry(ipset, &state->ipsets, list)
	{
		if (ipset->external)
			continue;

		delay = 5;
		for (tries = 0; !fw3_check_ipset(ipset) && tries < 10; tries++)
			usleep(delay<<1);
	}
}

void
fw3_destroy_ipsets(struct fw3_state *state, enum fw3_family family,
		   bool reload_set)
{
	unsigned int delay, tries;
	bool exec = false;
	struct fw3_ipset *ipset;

	if (state->disable_ipsets)
		return;

	/* destroy ipsets */
	list_for_each_entry(ipset, &state->ipsets, list)
	{
		if (ipset->family != family ||
		    (reload_set && !ipset->reload_set))
			continue;

		if (!exec)
		{
			exec = fw3_command_pipe(false, "ipset", "-exist", "-");

			if (!exec)
				return;
		}

		info(" * Deleting ipset %s", ipset->name);

		fw3_pr("flush %s\n", ipset->name);
		fw3_pr("destroy %s\n", ipset->name);
	}

	if (exec)
	{
		fw3_pr("quit\n");
		fw3_command_close();
	}

	/* wait for ipsets to disappear */
	list_for_each_entry(ipset, &state->ipsets, list)
	{
		if (ipset->external)
			continue;

		delay = 5;
		for (tries = 0; fw3_check_ipset(ipset) && tries < 10; tries++)
			usleep(delay<<1);
	}
}

struct fw3_ipset *
fw3_lookup_ipset(struct fw3_state *state, const char *name)
{
	struct fw3_ipset *s;

	if (list_empty(&state->ipsets))
		return NULL;

	list_for_each_entry(s, &state->ipsets, list)
	{
		if (strcmp(s->name, name))
			continue;

		return s;
	}

	return NULL;
}

bool
fw3_check_ipset(struct fw3_ipset *set)
{
	bool rv = false;

	socklen_t sz;
	int s = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
	struct ip_set_req_version req_ver;
	struct ip_set_req_get_set req_name;

	if (s < 0 || fcntl(s, F_SETFD, FD_CLOEXEC))
		goto out;

	sz = sizeof(req_ver);
	req_ver.op = IP_SET_OP_VERSION;

	if (getsockopt(s, SOL_IP, SO_IP_SET, &req_ver, &sz))
		goto out;

	sz = sizeof(req_name);
	req_name.op = IP_SET_OP_GET_BYNAME;
	req_name.version = req_ver.version;
	snprintf(req_name.set.name, IPSET_MAXNAMELEN - 1, "%s",
	         set->external ? set->external : set->name);

	if (getsockopt(s, SOL_IP, SO_IP_SET, &req_name, &sz))
		goto out;

	rv = ((sz == sizeof(req_name)) && (req_name.set.index != IPSET_INVALID_ID));

out:
	if (s >= 0)
		close(s);

	return rv;
}

void
fw3_ipsets_update_run_state(enum fw3_family family, struct fw3_state *run_state,
			    struct fw3_state *cfg_state)
{
	struct fw3_ipset *ipset_run, *ipset_cfg;
	bool in_cfg;

	list_for_each_entry(ipset_run, &run_state->ipsets, list) {
		if (ipset_run->family != family)
			continue;

		in_cfg = false;

		list_for_each_entry(ipset_cfg, &cfg_state->ipsets, list) {
			if (ipset_cfg->family != family)
				continue;

			if (strlen(ipset_run->name) ==
			    strlen(ipset_cfg->name) &&
			    !strcmp(ipset_run->name, ipset_cfg->name)) {
				in_cfg = true;
				break;
			}
		}

		/* If a set is found in run_state, but not in cfg_state then the
		 * set has been deleted/renamed. Set reload_set to true to force
		 * the old set to be destroyed in the "stop" fase of the reload.
		 * If the set is found, then copy the reload_set value from the
		 * configuration state. This ensures that the elements are
		 * always updated according to the configuration, and not the
		 * runtime state (which the user might have forgotten).
		 */
		if (!in_cfg)
			ipset_run->reload_set = true;
		else
			ipset_run->reload_set = ipset_cfg->reload_set;
	}
}
