/*
 * Copyright (C) 2009-2011 Martin Willi
 *
 * Copyright (C) secunet Security Networks 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.
 */

#include "ha_kernel.h"

typedef uint32_t u32;
typedef uint8_t u8;

#include <sys/utsname.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define CLUSTERIP_DIR "/proc/net/ipt_CLUSTERIP"

/**
 * Versions of jhash used in the Linux kernel
 */
typedef enum {
	/* old variant, http://burtleburtle.net/bob/c/lookup2.c */
	JHASH_LOOKUP2,
	/* new variant, http://burtleburtle.net/bob/c/lookup3.c, since 2.6.37 */
	JHASH_LOOKUP3,
	/* variant with different init values, since 4.1 */
	JHASH_LOOKUP3_1,
} jhash_version_t;

typedef struct private_ha_kernel_t private_ha_kernel_t;

/**
 * Private data of an ha_kernel_t object.
 */
struct private_ha_kernel_t {

	/**
	 * Public ha_kernel_t interface.
	 */
	ha_kernel_t public;

	/**
	 * Total number of ClusterIP segments
	 */
	u_int count;

	/**
	 * jhash version the kernel uses
	 */
	jhash_version_t version;
};

/**
 * Get the jhash version based on the uname().release
 */
static jhash_version_t get_jhash_version()
{
	struct utsname utsname;
	int a, b, c;

	if (uname(&utsname) == 0)
	{
		switch (sscanf(utsname.release, "%d.%d.%d", &a, &b, &c))
		{
			case 3:
				if (a == 2 && b == 6)
				{
					if (c < 37)
					{
						DBG1(DBG_CFG, "detected Linux %d.%d.%d, using old "
							 "jhash", a, b, c);
						return JHASH_LOOKUP2;
					}
					DBG1(DBG_CFG, "detected Linux %d.%d.%d, using new "
						 "jhash", a, b, c);
					return JHASH_LOOKUP3;
				}
				/* FALL */
			case 2:
				if (a < 4 || (a == 4 && b == 0))
				{
					DBG1(DBG_CFG, "detected Linux %d.%d, using new jhash",
						 a, b);
					return JHASH_LOOKUP3;
				}
				DBG1(DBG_CFG, "detected Linux %d.%d, using new jhash with "
					 "updated init values", a, b);
				return JHASH_LOOKUP3_1;
			default:
				break;
		}
	}
	DBG1(DBG_CFG, "detecting Linux version failed, using new jhash");
	return JHASH_LOOKUP3;
}

/**
 * Rotate 32 bit word x by k bits
 */
#define jhash_rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

/**
 * jhash algorithm of two words, as used in kernel (using 0 as initval)
 */
static uint32_t jhash(jhash_version_t version, uint32_t a, uint32_t b)
{
	uint32_t c = 0;

	switch (version)
	{
		case JHASH_LOOKUP2:
			a += 0x9e3779b9;
			b += 0x9e3779b9;

			a -= b; a -= c; a ^= (c >> 13);
			b -= c; b -= a; b ^= (a <<  8);
			c -= a; c -= b; c ^= (b >> 13);
			a -= b; a -= c; a ^= (c >> 12);
			b -= c; b -= a; b ^= (a << 16);
			c -= a; c -= b; c ^= (b >>  5);
			a -= b; a -= c; a ^= (c >>  3);
			b -= c; b -= a; b ^= (a << 10);
			c -= a; c -= b; c ^= (b >> 15);
			break;
		case JHASH_LOOKUP3_1:
			/* changed with 4.1: # of 32-bit words shifted by 2 and c is
			 * initialized. we only use the two word variant with SPIs, so it's
			 * unlikely that b is 0 in that case */
			c += ((b ? 2 : 1) << 2) + 0xdeadbeef;
			a += ((b ? 2 : 1) << 2);
			b += ((b ? 2 : 1) << 2);
			/* FALL */
		case JHASH_LOOKUP3:
			a += 0xdeadbeef;
			b += 0xdeadbeef;

			c ^= b; c -= jhash_rot(b, 14);
			a ^= c; a -= jhash_rot(c, 11);
			b ^= a; b -= jhash_rot(a, 25);
			c ^= b; c -= jhash_rot(b, 16);
			a ^= c; a -= jhash_rot(c,  4);
			b ^= a; b -= jhash_rot(a, 14);
			c ^= b; c -= jhash_rot(b, 24);
			break;
	}
	return c;
}

/**
 * Segment a calculated hash
 */
static u_int hash2segment(private_ha_kernel_t *this, uint64_t hash)
{
	return ((hash * this->count) >> 32) + 1;
}

/**
 * Get a host as an integer for hashing
 */
static uint32_t host2int(host_t *host)
{
	if (host->get_family(host) == AF_INET)
	{
		return *(uint32_t*)host->get_address(host).ptr;
	}
	return 0;
}

METHOD(ha_kernel_t, get_segment, u_int,
	private_ha_kernel_t *this, host_t *host)
{
	unsigned long hash;
	uint32_t addr;

	addr = host2int(host);
	hash = jhash(this->version, ntohl(addr), 0);

	return hash2segment(this, hash);
}

METHOD(ha_kernel_t, get_segment_spi, u_int,
	private_ha_kernel_t *this, host_t *host, uint32_t spi)
{
	unsigned long hash;
	uint32_t addr;

	addr = host2int(host);
	hash = jhash(this->version, ntohl(addr), ntohl(spi));

	return hash2segment(this, hash);
}

METHOD(ha_kernel_t, get_segment_int, u_int,
	private_ha_kernel_t *this, int n)
{
	unsigned long hash;

	hash = jhash(this->version, ntohl(n), 0);

	return hash2segment(this, hash);
}

/**
 * Activate/Deactivate a segment for a given clusterip file
 */
static void enable_disable(private_ha_kernel_t *this, u_int segment,
						   char *file, bool enable)
{
	char cmd[8];
	int fd;

	snprintf(cmd, sizeof(cmd), "%c%d\n", enable ? '+' : '-', segment);

	fd = open(file, O_WRONLY);
	if (fd == -1)
	{
		DBG1(DBG_CFG, "opening CLUSTERIP file '%s' failed: %s",
			 file, strerror(errno));
		return;
	}
	if (write(fd, cmd, strlen(cmd)) == -1)
	{
		DBG1(DBG_CFG, "writing to CLUSTERIP file '%s' failed: %s",
			 file, strerror(errno));
	}
	close(fd);
}

/**
 * Get the currently active segments in the kernel for a clusterip file
 */
static segment_mask_t get_active(private_ha_kernel_t *this, char *file)
{
	char buf[256];
	segment_mask_t mask = 0;
	ssize_t len;
	int fd;

	fd = open(file, O_RDONLY);
	if (fd == -1)
	{
		DBG1(DBG_CFG, "opening CLUSTERIP file '%s' failed: %s",
			 file, strerror(errno));
		return 0;
	}
	len = read(fd, buf, sizeof(buf)-1);
	close(fd);
	if (len == -1)
	{
		DBG1(DBG_CFG, "reading from CLUSTERIP file '%s' failed: %s",
			 file, strerror(errno));
	}
	else
	{
		enumerator_t *enumerator;
		u_int segment;
		char *token;

		buf[len] = '\0';
		enumerator = enumerator_create_token(buf, ",", " ");
		while (enumerator->enumerate(enumerator, &token))
		{
			segment = atoi(token);
			if (segment)
			{
				mask |= SEGMENTS_BIT(segment);
			}
		}
		enumerator->destroy(enumerator);
	}
	return mask;
}

METHOD(ha_kernel_t, activate, void,
	private_ha_kernel_t *this, u_int segment)
{
	enumerator_t *enumerator;
	char *file;

	enumerator = enumerator_create_directory(CLUSTERIP_DIR);
	if (enumerator)
	{
		while (enumerator->enumerate(enumerator, NULL, &file, NULL))
		{
			enable_disable(this, segment, file, TRUE);
		}
		enumerator->destroy(enumerator);
	}
}

METHOD(ha_kernel_t, deactivate, void,
	private_ha_kernel_t *this, u_int segment)
{
	enumerator_t *enumerator;
	char *file;

	enumerator = enumerator_create_directory(CLUSTERIP_DIR);
	if (enumerator)
	{
		while (enumerator->enumerate(enumerator, NULL, &file, NULL))
		{
			enable_disable(this, segment, file, FALSE);
		}
		enumerator->destroy(enumerator);
	}
}

/**
 * Disable all not-yet disabled segments on all clusterip addresses
 */
static void disable_all(private_ha_kernel_t *this)
{
	enumerator_t *enumerator;
	segment_mask_t active;
	char *file;
	int i;

	enumerator = enumerator_create_directory(CLUSTERIP_DIR);
	if (enumerator)
	{
		while (enumerator->enumerate(enumerator, NULL, &file, NULL))
		{
			if (chown(file, lib->caps->get_uid(lib->caps),
					  lib->caps->get_gid(lib->caps)) != 0)
			{
				DBG1(DBG_CFG, "changing ClusterIP permissions failed: %s",
					 strerror(errno));
			}
			active = get_active(this, file);
			for (i = 1; i <= this->count; i++)
			{
				if (active & SEGMENTS_BIT(i))
				{
					enable_disable(this, i, file, FALSE);
				}
			}
		}
		enumerator->destroy(enumerator);
	}
}

METHOD(ha_kernel_t, destroy, void,
	private_ha_kernel_t *this)
{
	free(this);
}

/**
 * See header
 */
ha_kernel_t *ha_kernel_create(u_int count)
{
	private_ha_kernel_t *this;

	INIT(this,
		.public = {
			.get_segment = _get_segment,
			.get_segment_spi = _get_segment_spi,
			.get_segment_int = _get_segment_int,
			.activate = _activate,
			.deactivate = _deactivate,
			.destroy = _destroy,
		},
		.version = get_jhash_version(),
		.count = count,
	);

	disable_all(this);

	return &this->public;
}

