/*
 * Soft:        Keepalived is a failover program for the LVS project
 *              <www.linuxvirtualserver.org>. It monitor & manipulate
 *              a loadbalanced server pool using multi-layer checks.
 *
 * Part:        ipset manipulation used in conjunction with iptables
 *
 * Author:      Quentin Armitage, <quentin@armitage.org.uk>
 *
 *              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.
 *
 *              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.
 *
 * Copyright (C) 2001-2017 Alexandre Cassen, <acassen@gmail.com>
 */

/* We dynamically attempt to load the library "by hand", since keepalived
 * may have been built on a system with ipsets, but the target system may
 * not have the ipset libraries installed.
 *
 * If the ipset libraries are not installed, keepalived will fallback to
 * adding entries into iptables.
 */

#include "config.h"

#include <net/if.h>		/* Force inclusion of net/if.h before linux/if.h */
#define LIBIPSET_NFPROTO_H
#define LIBIPSET_NF_INET_ADDR_H
#include <libipset/session.h>
#include <libipset/types.h>
#ifndef LIBIPSET_PRE_V7_COMPAT
#include <libipset/ipset.h>
#endif
#include <netinet/in.h>
#include <linux/types.h>	/* For __beXX types in userland */
#include <linux/netfilter.h>	/* For nf_inet_addr */
#include <stdint.h>

#include "logger.h"
#include "global_data.h"
#include "vrrp_ipset.h"
#include "vrrp_firewall.h"
#include "vrrp_iptables_calls.h"
#include "main.h"
#include "utils.h"

#ifdef _LIBIPSET_DYNAMIC_
#include <dlfcn.h>

/* The addresses of the functions we want */
#ifdef LIBIPSET_PRE_V7_COMPAT
struct ipset_session* (*ipset_session_init_addr)(ipset_outfn outfn);
#else
struct ipset_session* (*ipset_session_init_addr)(ipset_print_outfn outfn, const void *p);
#endif
int (*ipset_session_fini_addr)(struct ipset_session *session);
struct ipset_data* (*ipset_session_data_addr)(const struct ipset_session *session);
#ifdef LIBIPSET_PRE_V7_COMPAT
int (*ipset_envopt_parse_addr)(struct ipset_session *session, int env, const char *str);
#else
void (*ipset_envopt_set_addr)(struct ipset_session *session, int env);
#endif
const struct ipset_type* (*ipset_type_get_addr)(struct ipset_session *session, enum ipset_cmd cmd);
int (*ipset_data_set_addr)(struct ipset_data *data, enum ipset_opt opt, const void *value);
int (*ipset_cmd_addr)(struct ipset_session *session, enum ipset_cmd cmd, uint32_t lineno);
void (*ipset_load_types_addr)(void);

/* We can (almost) make it look as though normal linking is being used */
#define ipset_session_init (*ipset_session_init_addr)
#define ipset_session_fini (*ipset_session_fini_addr)
#define ipset_session_data (*ipset_session_data_addr)
#ifdef LIBIPSET_PRE_V7_COMPAT
#define ipset_envopt_parse (*ipset_envopt_parse_addr)
#else
#define ipset_envopt_set (*ipset_envopt_set_addr)
#endif
#define ipset_type_get (*ipset_type_get_addr)
#define ipset_data_set (*ipset_data_set_addr)
/* Unfortunately ipset_cmd conflicts with struct ipset_cmd */
#define ipset_cmd1 (*ipset_cmd_addr)
#define ipset_load_types (*ipset_load_types_addr)

static void* libipset_handle;
#else
#define ipset_cmd1 ipset_cmd
#endif

static int
#ifdef LIBIPSET_PRE_V7_COMPAT
__attribute__ ((format(printf, 1, 2)))
ipset_printf(const char *fmt, ...)
#else
__attribute__ ((format(printf, 3, 4)))
ipset_printf(__attribute ((__unused__)) struct ipset_session *session, void *p, const char *fmt, ...)
#endif
{
	va_list args;

#ifndef LIBIPSET_PRE_V7_COMPAT
	log_message(LOG_INFO, "libipset message from %s", PTR_CAST_CONST(char, p));
#endif

	va_start(args, fmt);
	vlog_message(LOG_INFO, fmt, args);
	va_end(args);

	return 0;
}

static bool
do_ipset_cmd(struct ipset_session* session, enum ipset_cmd cmd, const char *setname,
		const ip_address_t *addr, int cidr, uint32_t timeout, const char* iface)
{
	const struct ipset_type *type;
	uint8_t family;
	int r;

	ipset_session_data_set(session, IPSET_SETNAME, setname);

	type = ipset_type_get(session, cmd);
	if (type == NULL) {
		/* possible reasons for failure: set name does not exist */
		return false;
	}

	family = (addr->ifa.ifa_family == AF_INET) ? NFPROTO_IPV4 : NFPROTO_IPV6;
	ipset_session_data_set(session, IPSET_OPT_FAMILY, &family);
	ipset_session_data_set(session, IPSET_OPT_IP, &addr->u);
	if (cidr >= 0)
		ipset_session_data_set(session, IPSET_OPT_CIDR, &cidr);
	if (timeout)
		ipset_session_data_set(session, IPSET_OPT_TIMEOUT, &timeout);
	if (iface)
		ipset_session_data_set(session, IPSET_OPT_IFACE, iface);

	r = ipset_cmd1(session, cmd, 0);

	return r == 0;
}

static bool
ipset_create(struct ipset_session* session, const char *setname, const char *typename, uint8_t family)
{
	const struct ipset_type *type;
	int r;

	ipset_session_data_set(session, IPSET_SETNAME, setname);

	ipset_session_data_set(session, IPSET_OPT_TYPENAME, typename);

	type = ipset_type_get(session, IPSET_CMD_CREATE);
	if (type == NULL)
		return false;

	ipset_session_data_set(session, IPSET_OPT_TYPE, type);
	ipset_session_data_set(session, IPSET_OPT_FAMILY, &family);

	r = ipset_cmd1(session, IPSET_CMD_CREATE, 0);
	return r == 0;
}

static bool
ipset_destroy(struct ipset_session* session, const char *setname)
{
	int r;

	ipset_session_data_set(session, IPSET_SETNAME, setname);

	r = ipset_cmd1(session, IPSET_CMD_DESTROY, 0);
	return r == 0;
}

static bool
has_ipset_setname(struct ipset_session* session, const char *setname)
{
	ipset_session_data_set(session, IPSET_SETNAME, setname);

	return ipset_cmd1(session, IPSET_CMD_HEADER, 0) == 0;
}

static bool
create_sets(struct ipset_session **session, const char* addr4, const char* addr6, const char* addr_if6,
		const char *igmp, const char *mld,
#ifndef _HAVE_VRRP_VMAC_
		__attribute__((unused))
#endif
					const char *vmac_nd, bool is_reload)
{
	if (!*session)
#ifdef LIBIPSET_PRE_V7_COMPAT
		*session = ipset_session_init(ipset_printf);
#else
		*session = ipset_session_init(ipset_printf, no_const(char, "create_sets"));
#endif
	if (!*session) {
		log_message(LOG_INFO, "Cannot initialize ipset session.");
		return false;
	}

	/* If we aren't reloading, don't worry if sets already exists. With the
	 * IPSET_ENV_EXIST option set, any existing entries in the set are removed. */
	if (!is_reload)
#ifdef LIBIPSET_PRE_V7_COMPAT
		ipset_envopt_parse(*session, IPSET_ENV_EXIST, NULL);
#else
		ipset_envopt_set(*session, IPSET_ENV_EXIST);
#endif

	if (addr4) {
		if (!is_reload || !has_ipset_setname(*session, addr4))
			ipset_create(*session, addr4, "hash:ip", NFPROTO_IPV4);
	}

	if (addr6) {
		if (!is_reload || !has_ipset_setname(*session, addr6))
			ipset_create(*session, addr6, "hash:ip", NFPROTO_IPV6);
		if (!is_reload || !has_ipset_setname(*session, addr_if6))
			ipset_create(*session, addr_if6, "hash:net,iface", NFPROTO_IPV6);
	}

	if (igmp) {
		if (!is_reload || !has_ipset_setname(*session, igmp))
			ipset_create(*session, igmp, "hash:net,iface", NFPROTO_IPV4);
	}

	if (mld) {
		if (!is_reload || !has_ipset_setname(*session, mld))
			ipset_create(*session, mld, "hash:net,iface", NFPROTO_IPV6);
	}

#ifdef _HAVE_VRRP_VMAC_
	if (vmac_nd) {
		if (!is_reload || !has_ipset_setname(*session, vmac_nd))
			ipset_create(*session, vmac_nd, "hash:net,iface", NFPROTO_IPV6);
	}
#endif

	return true;
}

static
bool set_match_loaded(void)
{
	char buf[XT_FUNCTION_MAXNAMELEN+1];
	FILE *fp;
	bool found = false;

	fp = fopen("/proc/net/ip_tables_matches", "re");
	if (!fp)
		return false;

	while (fgets(buf, sizeof(buf), fp)) {
		if ((buf[3] == '\0' || buf[3] == '\n') &&
		    !strncmp(buf, "set", 3)) {
			found = true;
			break;
		}
	}

	fclose(fp);

	return found;
}

bool ipset_initialise(void)
{
#ifdef _LIBIPSET_DYNAMIC_
	if (libipset_handle)
		return true;
#endif

	/* Don't attempt to use ipsets if running in a namespace and the default
	 * set names have not been overridden and the kernel version is less
	 * than Linux 3.13, since ipsets didn't understand namespaces prior to that. */
	if (global_data->network_namespace &&
	    !global_data->namespace_with_ipsets &&
	    !strcmp(global_data->vrrp_ipset_address, DEFAULT_IPSET_NAME) &&
	    (os_major <= 2 ||
	     (os_major == 3 && os_minor < 13))) {
		log_message(LOG_INFO, "Not using ipsets with network namespace since not supported with kernel version < 3.13");
		return false;
	}

#ifdef _LIBIPSET_DYNAMIC_
	/* Attempt to open the ipset library */
	if (!(libipset_handle = dlopen("libipset.so", RTLD_NOW)) &&
	    !(libipset_handle = dlopen(IPSET_LIB_NAME, RTLD_NOW))) {
		log_message(LOG_INFO, "Unable to load ipset library - %s", dlerror());
		return false;
	}

	if (!(ipset_session_init_addr = dlsym(libipset_handle, "ipset_session_init")) ||
	    !(ipset_session_fini_addr = dlsym(libipset_handle, "ipset_session_fini")) ||
	    !(ipset_session_data_addr = dlsym(libipset_handle,"ipset_session_data")) ||
#ifdef LIBIPSET_PRE_V7_COMPAT
	    !(ipset_envopt_parse_addr = dlsym(libipset_handle,"ipset_envopt_parse")) ||
#else
	    !(ipset_envopt_set_addr = dlsym(libipset_handle,"ipset_envopt_set")) ||
#endif
	    !(ipset_type_get_addr = dlsym(libipset_handle,"ipset_type_get")) ||
	    !(ipset_data_set_addr = dlsym(libipset_handle,"ipset_data_set")) ||
	    !(ipset_cmd_addr = dlsym(libipset_handle,"ipset_cmd")) ||
	    !(ipset_load_types_addr = dlsym(libipset_handle,"ipset_load_types"))) {
		log_message(LOG_INFO, "Failed to dynamic link an ipset function - %s", dlerror());
		return false;
	}
#endif

	ipset_load_types();

	if (!set_match_loaded() && !keepalived_modprobe("xt_set")) {
		log_message(LOG_INFO, "Unable to load module xt_set - not using ipsets");
		return false;
	}

	return true;
}

// TODO - just revert to single call to remove_ipsets
static bool
remove_ipsets(struct ipset_session **session, uint8_t family, bool vip_sets)
{
	if (!global_data->using_ipsets)
		return true;

#ifdef _LIBIPSET_DYNAMIC_
	if (!libipset_handle)
		return true;
#endif

	if (!*session)
#ifdef LIBIPSET_PRE_V7_COMPAT
		*session = ipset_session_init(ipset_printf);
#else
		*session = ipset_session_init(ipset_printf, no_const(char, "remove_ipsets"));
#endif
	if (!*session) {
		log_message(LOG_INFO, "Cannot initialize ipset session.");
		return false;
	}

	if (vip_sets) {
		if (family == AF_INET)
			ipset_destroy(*session, global_data->vrrp_ipset_address);
		else {
			ipset_destroy(*session, global_data->vrrp_ipset_address6);
			ipset_destroy(*session, global_data->vrrp_ipset_address_iface6);
		}
	} else {
		if (family == AF_INET)
			ipset_destroy(*session, global_data->vrrp_ipset_igmp);
		else {
			ipset_destroy(*session, global_data->vrrp_ipset_mld);
#ifdef _HAVE_VRRP_VMAC_
			ipset_destroy(*session, global_data->vrrp_ipset_vmac_nd);
#endif
		}
	}

	return true;
}

bool
remove_vip_ipsets(struct ipset_session **session, uint8_t family)
{
	return remove_ipsets(session, family, true);
}

bool
remove_igmp_ipsets(struct ipset_session **session, uint8_t family)
{
	return remove_ipsets(session, family, false);
}

bool add_vip_ipsets(struct ipset_session **session, uint8_t family, bool is_reload)
{
	if (family == AF_INET)
		return create_sets(session, global_data->vrrp_ipset_address, NULL, NULL, NULL, NULL, NULL, is_reload);

	return create_sets(session, NULL, global_data->vrrp_ipset_address6, global_data->vrrp_ipset_address_iface6, NULL, NULL, NULL, is_reload);
}

bool add_igmp_ipsets(struct ipset_session **session, uint8_t family, bool is_reload)
{
	if (family == AF_INET)
		return create_sets(session, NULL, NULL, NULL, global_data->vrrp_ipset_igmp, NULL, NULL, is_reload);

	return create_sets(session, NULL, NULL, NULL, NULL, global_data->vrrp_ipset_mld, NULL, is_reload)
#ifdef _HAVE_VRRP_VMAC_
		&& create_sets(session, NULL, NULL, NULL, NULL, NULL, global_data->vrrp_ipset_vmac_nd, is_reload)
#endif
		;
}

void* ipset_session_start(void)
{
#ifdef LIBIPSET_PRE_V7_COMPAT
	return ipset_session_init(ipset_printf);
#else
	return ipset_session_init(ipset_printf, no_const(char, "session_start"));
#endif
}

void ipset_session_end(void* vsession)
{
	struct ipset_session *session = vsession;

	ipset_session_fini(session);
}

void ipset_entry(void* vsession, int cmd, const ip_address_t* addr)
{
	const char* set;
	char *iface = NULL;
	struct ipset_session *session = vsession;


	if (addr->ifa.ifa_family == AF_INET)
		set = global_data->vrrp_ipset_address;
	else if (IN6_IS_ADDR_LINKLOCAL(&addr->u.sin6_addr)) {
		set = global_data->vrrp_ipset_address_iface6;
		iface = addr->ifp->ifname;
	}
	else
		set = global_data->vrrp_ipset_address6;

	do_ipset_cmd(session, (cmd == IPADDRESS_DEL) ? IPSET_CMD_DEL : IPSET_CMD_ADD, set, addr, -1, 0, iface);
}

void ipset_entry_igmp(void* vsession, int cmd, const char* ifname, uint8_t family)
{
	const char* set;
	struct ipset_session *session = vsession;
	ip_address_t addr = { .ifa.ifa_family = AF_INET };

	if (family == AF_INET) {
		set = global_data->vrrp_ipset_igmp;
	} else {
		set = global_data->vrrp_ipset_mld;
		addr.ifa.ifa_family = AF_INET6;
	}

	do_ipset_cmd(session, (cmd == IPADDRESS_DEL) ? IPSET_CMD_DEL : IPSET_CMD_ADD, set, &addr, 0, 0, ifname);
}

#ifdef _HAVE_VRRP_VMAC_
void ipset_entry_nd(void* vsession, int cmd, const interface_t* ifp)
{
	struct ipset_session *session = vsession;
	ip_address_t addr = { .ifa.ifa_family = AF_INET6, .u.sin6_addr = ifp->base_ifp->sin6_addr };


	do_ipset_cmd(session, (cmd == IPADDRESS_DEL) ? IPSET_CMD_DEL : IPSET_CMD_ADD, global_data->vrrp_ipset_vmac_nd, &addr, -1, 0, ifp->ifname);
}
#endif

void
set_default_ipsets(void)
{
	global_data->vrrp_ipset_address = STRDUP(DEFAULT_IPSET_NAME);
	global_data->vrrp_ipset_address6 = STRDUP(DEFAULT_IPSET_NAME "6");
	global_data->vrrp_ipset_address_iface6 = STRDUP(DEFAULT_IPSET_NAME "_if6");
	global_data->vrrp_ipset_igmp = STRDUP(DEFAULT_IPSET_NAME "_igmp");
	global_data->vrrp_ipset_mld = STRDUP(DEFAULT_IPSET_NAME "_mld");
#ifdef _HAVE_VRRP_VMAC_
	global_data->vrrp_ipset_vmac_nd = STRDUP(DEFAULT_IPSET_NAME "_nd");
#endif
}

void
disable_ipsets(void)
{
	global_data->using_ipsets = false;
	FREE_CONST_PTR(global_data->vrrp_ipset_address);
	FREE_CONST_PTR(global_data->vrrp_ipset_address6);
	FREE_CONST_PTR(global_data->vrrp_ipset_address_iface6);
	FREE_CONST_PTR(global_data->vrrp_ipset_igmp);
	FREE_CONST_PTR(global_data->vrrp_ipset_mld);
#ifdef _HAVE_VRRP_VMAC_
	FREE_CONST_PTR(global_data->vrrp_ipset_vmac_nd);
#endif
}
