/*
 * Copyright (C) 2013 Martin Willi
 * Copyright (C) 2013 revosec AG
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

/* Windows 7, for some iphlpapi.h functionality */
#define _WIN32_WINNT 0x0601
#include <winsock2.h>
#include <ws2ipdef.h>
#include <windows.h>
#include <ntddndis.h>
#include <naptypes.h>
#include <iphlpapi.h>

#include "kernel_iph_net.h"

#include <daemon.h>
#include <threading/mutex.h>
#include <collections/linked_list.h>
#include <processing/jobs/callback_job.h>


/** delay before firing roam events (ms) */
#define ROAM_DELAY 500

typedef struct private_kernel_iph_net_t private_kernel_iph_net_t;

/**
 * Private data of kernel_iph_net implementation.
 */
struct private_kernel_iph_net_t {

	/**
	 * Public interface.
	 */
	kernel_iph_net_t public;

	/**
	 * NotifyIpInterfaceChange() handle
	 */
	HANDLE changes;

	/**
	 * EnableRouter() OVERLAPPED
	 */
	OVERLAPPED router;

	/**
	 * Mutex to access interface list
	 */
	mutex_t *mutex;

	/**
	 * Known interfaces, as iface_t
	 */
	linked_list_t *ifaces;

	/**
	 * Earliest time of the next roam event
	 */
	timeval_t roam_next;

	/**
	 * Roam event due to address change?
	 */
	bool roam_address;
};

/**
 * Interface entry
 */
typedef struct  {
	/** interface index */
	DWORD ifindex;
	/** interface name */
	char *ifname;
	/** interface description */
	char *ifdesc;
	/** type of interface */
	DWORD iftype;
	/** interface status */
	IF_OPER_STATUS status;
	/** list of known addresses, as host_t */
	linked_list_t *addrs;
} iface_t;

/**
 * Clean up an iface_t
 */
static void iface_destroy(iface_t *this)
{
	this->addrs->destroy_offset(this->addrs, offsetof(host_t, destroy));
	free(this->ifname);
	free(this->ifdesc);
	free(this);
}

/**
 * Enum names for Windows IF_OPER_STATUS
 */
ENUM(if_oper_names, IfOperStatusUp, IfOperStatusLowerLayerDown,
	"Up",
	"Down",
	"Testing",
	"Unknown",
	"Dormant",
	"NotPresent",
	"LowerLayerDown",
);

/**
 * Callback function that raises the delayed roam event
 */
static job_requeue_t roam_event(private_kernel_iph_net_t *this)
{
	bool address;

	this->mutex->lock(this->mutex);
	address = this->roam_address;
	this->roam_address = FALSE;
	this->mutex->unlock(this->mutex);

	charon->kernel->roam(charon->kernel, address);
	return JOB_REQUEUE_NONE;
}

/**
 * Fire delayed roam event, caller should hold mutex
 */
static void fire_roam_event(private_kernel_iph_net_t *this, bool address)
{
	timeval_t now;

	time_monotonic(&now);
	this->roam_address |= address;
	if (timercmp(&now, &this->roam_next, >))
	{
		timeval_add_ms(&now, ROAM_DELAY);
		this->roam_next = now;
		lib->scheduler->schedule_job_ms(lib->scheduler, (job_t*)
							callback_job_create((callback_job_cb_t)roam_event,
												this, NULL, NULL),
							ROAM_DELAY);
	}
}

/**
 * Update addresses for an iface entry
 */
static void update_addrs(private_kernel_iph_net_t *this, iface_t *entry,
						 IP_ADAPTER_ADDRESSES *addr, bool log)
{
	IP_ADAPTER_UNICAST_ADDRESS *current;
	enumerator_t *enumerator;
	linked_list_t *list;
	host_t *host, *old;
	bool changes = FALSE;

	list = entry->addrs;
	entry->addrs = linked_list_create();

	for (current = addr->FirstUnicastAddress; current; current = current->Next)
	{
		if (current->Address.lpSockaddr->sa_family == AF_INET6)
		{
			struct sockaddr_in6 *sin;

			sin = (struct sockaddr_in6*)current->Address.lpSockaddr;
			if (IN6_IS_ADDR_LINKLOCAL(&sin->sin6_addr))
			{
				continue;
			}
		}

		host = host_create_from_sockaddr(current->Address.lpSockaddr);
		if (host)
		{
			bool found = FALSE;

			enumerator = list->create_enumerator(list);
			while (enumerator->enumerate(enumerator, &old))
			{
				if (host->ip_equals(host, old))
				{
					list->remove_at(list, enumerator);
					old->destroy(old);
					found = TRUE;
				}
			}
			enumerator->destroy(enumerator);

			entry->addrs->insert_last(entry->addrs, host);

			if (!found && log)
			{
				DBG1(DBG_KNL, "%H appeared on interface %u '%s'",
					 host, entry->ifindex, entry->ifdesc);
				changes = TRUE;
			}
		}
	}

	while (list->remove_first(list, (void**)&old) == SUCCESS)
	{
		if (log)
		{
			DBG1(DBG_KNL, "%H disappeared from interface %u '%s'",
				 old, entry->ifindex, entry->ifdesc);
			changes = TRUE;
		}
		old->destroy(old);
	}
	list->destroy(list);

	if (changes)
	{
		fire_roam_event(this, TRUE);
	}
}

/**
 * Add an interface entry
 */
static void add_interface(private_kernel_iph_net_t *this,
						  IP_ADAPTER_ADDRESSES *addr, bool log)
{
	enumerator_t *enumerator;
	iface_t *entry;
	bool exists = FALSE;

	this->mutex->lock(this->mutex);
	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (entry->ifindex == addr->IfIndex)
		{
			exists = TRUE;
			break;
		}
	}
	enumerator->destroy(enumerator);
	this->mutex->unlock(this->mutex);

	if (!exists)
	{
		char desc[128] = "";

		wcstombs(desc, addr->Description, sizeof(desc));

		INIT(entry,
			.ifindex = addr->IfIndex,
			.ifname = strdup(addr->AdapterName),
			.ifdesc = strdup(desc),
			.iftype = addr->IfType,
			.status = addr->OperStatus,
			.addrs = linked_list_create(),
		);

		if (log)
		{
			DBG1(DBG_KNL, "interface %u '%s' appeared",
				 entry->ifindex, entry->ifdesc);
		}

		this->mutex->lock(this->mutex);
		update_addrs(this, entry, addr, log);
		this->ifaces->insert_last(this->ifaces, entry);
		this->mutex->unlock(this->mutex);
	}
}

/**
 * Remove an interface entry that is gone
 */
static void remove_interface(private_kernel_iph_net_t *this, NET_IFINDEX index)
{
	enumerator_t *enumerator;
	iface_t *entry;

	this->mutex->lock(this->mutex);
	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (entry->ifindex == index)
		{
			this->ifaces->remove_at(this->ifaces, enumerator);
			DBG1(DBG_KNL, "interface %u '%s' disappeared",
				 entry->ifindex, entry->ifdesc);
			iface_destroy(entry);
			fire_roam_event(this, TRUE);
		}
	}
	enumerator->destroy(enumerator);
	this->mutex->unlock(this->mutex);
}

/**
 * Update an interface entry changed
 */
static void update_interface(private_kernel_iph_net_t *this,
							 IP_ADAPTER_ADDRESSES *addr)
{
	enumerator_t *enumerator;
	iface_t *entry;

	this->mutex->lock(this->mutex);
	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (entry->ifindex == addr->IfIndex)
		{
			if (entry->status != addr->OperStatus)
			{
				DBG1(DBG_KNL, "interface %u '%s' changed state from %N to %N",
					 entry->ifindex, entry->ifdesc, if_oper_names,
					 entry->status, if_oper_names, addr->OperStatus);
				entry->status = addr->OperStatus;
				fire_roam_event(this, TRUE);
			}
			update_addrs(this, entry, addr, TRUE);
		}
	}
	enumerator->destroy(enumerator);
	this->mutex->unlock(this->mutex);
}

/**
 * MinGW gets MIB_IPINTERFACE_ROW wrong, as it packs InterfaceLuid just after
 * Family. Fix that with our own version of the struct header.
 */
typedef struct {
	ADDRESS_FAMILY Family;
	union {
		ULONG64 Value;
		struct {
			ULONG64 Reserved :24;
			ULONG64 NetLuidIndex :24;
			ULONG64 IfType :16;
		} Info;
	} InterfaceLuid;
	NET_IFINDEX InterfaceIndex;
	/* more would go here if needed */
} MIB_IPINTERFACE_ROW_FIXUP;

/**
 * NotifyIpInterfaceChange() callback
 */
static void WINAPI change_interface(void *user, PMIB_IPINTERFACE_ROW row_badal,
									MIB_NOTIFICATION_TYPE type)
{
	private_kernel_iph_net_t *this = user;
	MIB_IPINTERFACE_ROW_FIXUP* row = (MIB_IPINTERFACE_ROW_FIXUP*)row_badal;
	IP_ADAPTER_ADDRESSES addrs[64], *current;
	ULONG res, size = sizeof(addrs);

	if (row && type == MibDeleteInstance)
	{
		remove_interface(this, row->InterfaceIndex);
	}
	else
	{
		res = GetAdaptersAddresses(AF_UNSPEC,
						GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST |
						GAA_FLAG_SKIP_DNS_SERVER | GAA_FLAG_SKIP_FRIENDLY_NAME,
						NULL, addrs, &size);
		if (res == NO_ERROR)
		{
			current = addrs;
			while (current)
			{
				/* row is NULL only on MibInitialNotification */
				if (!row || row->InterfaceIndex == current->IfIndex)
				{
					switch (type)
					{
						case MibParameterNotification:
							update_interface(this, current);
							break;
						case MibInitialNotification:
							add_interface(this, current, FALSE);
							break;
						case MibAddInstance:
							add_interface(this, current, TRUE);
							break;
						default:
							break;
					}
				}
				current = current->Next;
			}
		}
		else
		{
			DBG1(DBG_KNL, "getting IPH adapter addresses failed: 0x%08lx", res);
		}
	}
}

/**
 * Get an iface entry for a local address, does no locking
 */
static iface_t* address2entry(private_kernel_iph_net_t *this, host_t *ip)
{
	enumerator_t *ifaces, *addrs;
	iface_t *entry, *found = NULL;
	host_t *host;

	ifaces = this->ifaces->create_enumerator(this->ifaces);
	while (!found && ifaces->enumerate(ifaces, &entry))
	{
		addrs = entry->addrs->create_enumerator(entry->addrs);
		while (!found && addrs->enumerate(addrs, &host))
		{
			if (host->ip_equals(host, ip))
			{
				found = entry;
			}
		}
		addrs->destroy(addrs);
	}
	ifaces->destroy(ifaces);

	return found;
}

METHOD(kernel_net_t, get_interface_name, bool,
	private_kernel_iph_net_t *this, host_t* ip, char **name)
{
	iface_t *entry;

	this->mutex->lock(this->mutex);
	entry = address2entry(this, ip);
	if (entry && name)
	{
		*name = strdup(entry->ifname);
	}
	this->mutex->unlock(this->mutex);

	return entry != NULL;
}

/**
 * Address enumerator
 */
typedef struct {
	/** implements enumerator_t */
	enumerator_t public;
	/** what kind of address should we enumerate? */
	kernel_address_type_t which;
	/** enumerator over interfaces */
	enumerator_t *ifaces;
	/** current enumerator over addresses, or NULL */
	enumerator_t *addrs;
	/** mutex to unlock on destruction */
	mutex_t *mutex;
} addr_enumerator_t;

METHOD(enumerator_t, addr_enumerate, bool,
	addr_enumerator_t *this, va_list args)
{
	iface_t *entry;
	host_t **host;

	VA_ARGS_VGET(args, host);

	while (TRUE)
	{
		while (!this->addrs)
		{
			if (!this->ifaces->enumerate(this->ifaces, &entry))
			{
				return FALSE;
			}
			if (entry->iftype == IF_TYPE_SOFTWARE_LOOPBACK &&
				!(this->which & ADDR_TYPE_LOOPBACK))
			{
				continue;
			}
			if (entry->status != IfOperStatusUp &&
				!(this->which & ADDR_TYPE_DOWN))
			{
				continue;
			}
			this->addrs = entry->addrs->create_enumerator(entry->addrs);
		}
		if (this->addrs->enumerate(this->addrs, host))
		{
			return TRUE;
		}
		this->addrs->destroy(this->addrs);
		this->addrs = NULL;
	}
}

METHOD(enumerator_t, addr_destroy, void,
	addr_enumerator_t *this)
{
	DESTROY_IF(this->addrs);
	this->ifaces->destroy(this->ifaces);
	this->mutex->unlock(this->mutex);
	free(this);
}

METHOD(kernel_net_t, create_address_enumerator, enumerator_t*,
	private_kernel_iph_net_t *this, kernel_address_type_t which)
{
	addr_enumerator_t *enumerator;

	if (!(which & ADDR_TYPE_REGULAR))
	{
		/* we currently have no virtual, but regular IPs only */
		return enumerator_create_empty();
	}

	this->mutex->lock(this->mutex);

	INIT(enumerator,
		.public = {
			.enumerate = enumerator_enumerate_default,
			.venumerate = _addr_enumerate,
			.destroy = _addr_destroy,
		},
		.which = which,
		.ifaces = this->ifaces->create_enumerator(this->ifaces),
		.mutex = this->mutex,
	);
	return &enumerator->public;
}

METHOD(kernel_net_t, get_source_addr, host_t*,
	private_kernel_iph_net_t *this, host_t *dest, host_t *src)
{
	MIB_IPFORWARD_ROW2 route;
	SOCKADDR_INET best, *sai_dst, *sai_src = NULL;
	DWORD res, index = 0;

	res = GetBestInterfaceEx(dest->get_sockaddr(dest), &index);
	if (res != NO_ERROR)
	{
		DBG1(DBG_KNL, "getting interface to %H failed: 0x%08x", dest, res);
		return NULL;
	}

	sai_dst = (SOCKADDR_INET*)dest->get_sockaddr(dest);
	if (src)
	{
		sai_src = (SOCKADDR_INET*)src->get_sockaddr(src);
	}
	res = GetBestRoute2(0, index, sai_src, sai_dst, 0, &route, &best);
	if (res != NO_ERROR)
	{
		DBG2(DBG_KNL, "getting src address to %H failed: 0x%08x", dest, res);
		return NULL;
	}
	return host_create_from_sockaddr((struct sockaddr*)&best);
}

METHOD(kernel_net_t, get_nexthop, host_t*,
	private_kernel_iph_net_t *this, host_t *dest, int prefix, host_t *src,
	char **iface)
{
	MIB_IPFORWARD_ROW2 route;
	SOCKADDR_INET best, *sai_dst, *sai_src = NULL;
	DWORD res, index = 0;
	host_t *nexthop;

	res = GetBestInterfaceEx(dest->get_sockaddr(dest), &index);
	if (res != NO_ERROR)
	{
		DBG1(DBG_KNL, "getting interface to %H failed: 0x%08x", dest, res);
		return NULL;
	}

	sai_dst = (SOCKADDR_INET*)dest->get_sockaddr(dest);
	if (src)
	{
		sai_src = (SOCKADDR_INET*)src->get_sockaddr(src);
	}
	res = GetBestRoute2(0, index, sai_src, sai_dst, 0, &route, &best);
	if (res != NO_ERROR)
	{
		DBG2(DBG_KNL, "getting nexthop to %H failed: 0x%08x", dest, res);
		return NULL;
	}
	nexthop = host_create_from_sockaddr((struct sockaddr*)&route.NextHop);
	if (nexthop)
	{
		if (!nexthop->is_anyaddr(nexthop))
		{
			if (iface)
			{
				*iface = NULL;
			}
			return nexthop;
		}
		nexthop->destroy(nexthop);
	}
	return NULL;
}

METHOD(kernel_net_t, add_ip, status_t,
	private_kernel_iph_net_t *this, host_t *virtual_ip, int prefix,
	char *iface_name)
{
	return NOT_SUPPORTED;
}

METHOD(kernel_net_t, del_ip, status_t,
	private_kernel_iph_net_t *this, host_t *virtual_ip, int prefix,
	bool wait)
{
	return NOT_SUPPORTED;
}

/**
 * Add or remove a route
 */
static status_t manage_route(private_kernel_iph_net_t *this, bool add,
					chunk_t dst, uint8_t prefixlen, host_t *gtw, char *name)
{
	MIB_IPFORWARD_ROW2 row = {
		.DestinationPrefix = {
			.PrefixLength = prefixlen,
		},
		.SitePrefixLength = prefixlen,
		.ValidLifetime = INFINITE,
		.PreferredLifetime = INFINITE,
		.Metric = 10,
		.Protocol = MIB_IPPROTO_NETMGMT,
	};
	enumerator_t *enumerator;
	iface_t *entry;
	ULONG ret;

	this->mutex->lock(this->mutex);
	enumerator = this->ifaces->create_enumerator(this->ifaces);
	while (enumerator->enumerate(enumerator, &entry))
	{
		if (streq(name, entry->ifname))
		{
			row.InterfaceIndex = entry->ifindex;
			break;
		}
	}
	enumerator->destroy(enumerator);
	this->mutex->unlock(this->mutex);

	if (!row.InterfaceIndex)
	{
		return NOT_FOUND;
	}
	switch (dst.len)
	{
		case 4:
			row.DestinationPrefix.Prefix.si_family = AF_INET;
			memcpy(&row.DestinationPrefix.Prefix.Ipv4.sin_addr,
				   dst.ptr, dst.len);
			break;
		case 16:
			row.DestinationPrefix.Prefix.si_family = AF_INET6;
			memcpy(&row.DestinationPrefix.Prefix.Ipv6.sin6_addr,
				   dst.ptr, dst.len);
			break;
		default:
			return FAILED;
	}
	if (gtw)
	{
		memcpy(&row.NextHop, gtw->get_sockaddr(gtw),
			   *gtw->get_sockaddr_len(gtw));
	}

	if (add)
	{
		ret = CreateIpForwardEntry2(&row);
	}
	else
	{
		ret = DeleteIpForwardEntry2(&row);
	}
	if (ret != NO_ERROR)
	{
		DBG1(DBG_KNL, "%sing route failed: 0x%08lx", add ? "add" : "remov", ret);
		return FAILED;
	}

	if (add)
	{
		ret = EnableRouter(NULL, &this->router);
		if (ret != ERROR_IO_PENDING)
		{
			DBG1(DBG_KNL, "EnableRouter router failed: 0x%08lx", ret);
		}
	}
	else
	{
		ret = UnenableRouter(&this->router, NULL);
		if (ret != NO_ERROR)
		{
			DBG1(DBG_KNL, "UnenableRouter router failed: 0x%08lx", ret);
		}
	}
	return SUCCESS;
}

METHOD(kernel_net_t, add_route, status_t,
	private_kernel_iph_net_t *this, chunk_t dst, uint8_t prefixlen,
	host_t *gateway, host_t *src, char *name, bool pass)
{
	return manage_route(this, TRUE, dst, prefixlen, gateway, name);
}

METHOD(kernel_net_t, del_route, status_t,
	private_kernel_iph_net_t *this, chunk_t dst, uint8_t prefixlen,
	host_t *gateway, host_t *src, char *name, bool pass)
{
	return manage_route(this, FALSE, dst, prefixlen, gateway, name);
}

METHOD(kernel_net_t, destroy, void,
	private_kernel_iph_net_t *this)
{
	if (this->changes)
	{
		CancelMibChangeNotify2(this->changes);
	}
	CloseHandle(this->router.hEvent);
	this->mutex->destroy(this->mutex);
	this->ifaces->destroy_function(this->ifaces, (void*)iface_destroy);
	free(this);
}

/*
 * Described in header.
 */
kernel_iph_net_t *kernel_iph_net_create()
{
	private_kernel_iph_net_t *this;
	ULONG res;

	INIT(this,
		.public = {
			.interface = {
				.get_interface = _get_interface_name,
				.create_address_enumerator = _create_address_enumerator,
				.get_source_addr = _get_source_addr,
				.get_nexthop = _get_nexthop,
				.add_ip = _add_ip,
				.del_ip = _del_ip,
				.add_route = _add_route,
				.del_route = _del_route,
				.destroy = _destroy,
			},
		},
		.router = {
			.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL),
		},
		.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
		.ifaces = linked_list_create(),
	);
	/* PIPINTERFACE_CHANGE_CALLBACK is not using WINAPI in MinGW, which seems
	 * to be wrong. Force a cast to our WINAPI call */
	res = NotifyIpInterfaceChange(AF_UNSPEC, (void*)change_interface,
								  this, TRUE, &this->changes);
	if (res != NO_ERROR)
	{
		DBG1(DBG_KNL, "registering for IPH interface changes failed: 0x%08lx",
			 res);
		destroy(this);
		return NULL;
	}

	return &this->public;
}
