/*
 *	Mobile IPv6 Mobile Node Module
 *
 *	Authors:
 *	Sami Kivisaari          <skivisaa@cc.hut.fi>
 *	Antti Tuominen          <ajtuomin@tml.hut.fi>
 *
 *	$Id$
 *
 *	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 <linux/config.h>
#include <linux/module.h>
#include <linux/init.h>

#ifdef CONFIG_SYSCTL
#include <linux/sysctl.h>
#endif /* CONFIG_SYSCTL */

#include <net/mipglue.h>

int mipv6_debug = 1;
int mipv6_use_auth = 0;

int mipv6_is_initialized = 0; /* XXX */

#if defined(MODULE) && LINUX_VERSION_CODE > 0x20115
MODULE_AUTHOR("MIPL Team");
MODULE_DESCRIPTION("Mobile IPv6");
MODULE_LICENSE("GPL");
MODULE_PARM(mipv6_debug, "i");
#endif

#include "config.h"
int mipv6_cap = 0;

#define MIPV6_BCACHE_SIZE 128
#define MIPV6_HALIST_SIZE 128

#include "bcache.h"
#include "stats.h"
#include "mobhdr.h"
#include "tunnel.h"
#include "mipv6_ioctl.h"
#include "exthdrs.h"
#include "mn.h"
#include "halist.h"
#include "prefix.h"
#include "mipv6_icmp.h"

#include "debug.h"
#include "mdetect.h"
#include "util.h"

/* 
 * Called from ndisc.c's router_discovery.
 */

static int mipv6_mn_ra_rcv(struct sk_buff *skb)
{
	int optlen, ha_info_pref = 0, ha_info_lifetime;
	int ifi = ((struct inet6_skb_parm *)skb->cb)->iif;
	struct ra_msg *ra = (struct ra_msg *) skb->h.raw;
	struct in6_addr *saddr = &skb->nh.ipv6h->saddr;
	struct router nrt;
	struct hal {
		struct in6_addr prefix;
		struct hal *next;
	};
	struct hal *ha_queue = NULL;

	__u8 * opt = (__u8 *)(ra + 1);

	DEBUG_FUNC();

	memset(&nrt, 0, sizeof(struct router));

	if (ra->icmph.icmp6_home_agent) {
		nrt.flags |= ND_RA_FLAG_HA;
		DEBUG(DBG_DATADUMP, "RA has ND_RA_FLAG_HA up");
	}

	if (ra->icmph.icmp6_addrconf_managed) {
		nrt.flags |= ND_RA_FLAG_MANAGED;
		DEBUG(DBG_DATADUMP, "RA has ND_RA_FLAG_MANAGED up");
	}

	if (ra->icmph.icmp6_addrconf_other) {
		nrt.flags |= ND_RA_FLAG_OTHER;
		DEBUG(DBG_DATADUMP, "RA has ND_RA_FLAG_OTHER up");
	}

	ha_info_lifetime = nrt.lifetime = 
		ntohs(ra->icmph.icmp6_rt_lifetime);
	ipv6_addr_copy(&nrt.ll_addr, saddr);
	nrt.ifindex = ifi;

	optlen = (skb->tail - (unsigned char *)ra) - sizeof(struct ra_msg);

	while (optlen > 0) {
		int len = (opt[1] << 3);
		if (len == 0)
			return MIPV6_IGN_RTR;
		
		if (opt[0] == ND_OPT_PREFIX_INFO) {
			struct prefix_info *pinfo;

			if (len < sizeof(struct prefix_info)) 
				return MIPV6_IGN_RTR;

			pinfo = (struct prefix_info *) opt;

			if (!pinfo->autoconf) {
				/* Autonomous not set according to
                                 * 2462 5.5.3 (a)
				 */
				goto nextopt;
			}

			if ((nrt.flags & ND_RA_FLAG_HA) && pinfo->router_address) {
				/* If RA has H bit set and Prefix Info
				 * Option R bit set, queue this
				 * address to be added to Home Agents
				 * List.  
				 */
				struct hal *tmp;
				if (ipv6_addr_type(&pinfo->prefix) & IPV6_ADDR_LINKLOCAL)
					goto nextopt;
				tmp = kmalloc(sizeof(struct hal), GFP_ATOMIC);
				if (tmp == NULL) {
					DEBUG(DBG_ERROR, "Out of memory");
					return MIPV6_IGN_RTR;
				}
				ipv6_addr_copy(&tmp->prefix, &pinfo->prefix);
				tmp->next = ha_queue;
				ha_queue = tmp;
			}
			if (!(nrt.flags & ND_RA_FLAG_HA) && !(ipv6_addr_type(
			    &pinfo->prefix) & IPV6_ADDR_LINKLOCAL)) {
				mipv6_delete_minfo_by_ha(&pinfo->prefix);
			}

			/* use first prefix with widest scope */
			if (ipv6_addr_any(&nrt.raddr) || 
			    ((ipv6_addr_type(&nrt.raddr) != IPV6_ADDR_UNICAST) &&
			    (ipv6_addr_type(&pinfo->prefix) == IPV6_ADDR_UNICAST))) {
				ipv6_addr_copy(&nrt.raddr, &pinfo->prefix);
				nrt.pfix_len = pinfo->prefix_len;
				if (pinfo->router_address)
					nrt.glob_addr = 1;
				else
					nrt.glob_addr = 0;
				DEBUG(DBG_DATADUMP, "Address of the received "
				      "prefix info option: %x:%x:%x:%x:%x:%x:%x:%x", 
				      NIPV6ADDR(&nrt.raddr));
				DEBUG(DBG_DATADUMP, "the length of the prefix is %d", 
				      nrt.pfix_len);
			}
		}
		if (opt[0] == ND_OPT_SOURCE_LL_ADDR) {
			nrt.link_addr_len = skb->dev->addr_len;
			memcpy(nrt.link_addr, opt + 2, nrt.link_addr_len);
		}
		if (opt[0] == ND_OPT_RTR_ADV_INTERVAL) {			
			nrt.interval = ntohl(*(__u32 *)(opt+4));
			DEBUG(DBG_DATADUMP, 
			      "received router interval option with interval : %d ",
			      nrt.interval / HZ);
			
			if (nrt.interval > MAX_RADV_INTERVAL) {
				nrt.interval = 0;
				DEBUG(DBG_DATADUMP, "but we are using: %d, "
				      "because interval>MAX_RADV_INTERVAL",
				      nrt.interval / HZ);
			}
		}
		if (opt[0] == ND_OPT_HOME_AGENT_INFO) {
			__u16 tmp;
			tmp = ntohs(*(__u16 *)(opt + 4));
			ha_info_pref = (tmp & 0x8000) ? -(int)((u16)(~0))^(tmp + 1) : tmp;
			ha_info_lifetime = ntohs(*(__u16 *)(opt + 6));
			DEBUG(DBG_DATADUMP,
			      "received home agent info with preference : %d and lifetime : %d",
				 ha_info_pref, ha_info_lifetime);
		}
	nextopt:
		optlen -= len;
		opt += len;
	}
	while (ha_queue) {
		struct hal *tmp = ha_queue->next;
		if (ha_info_lifetime) {
			mipv6_halist_add(ifi, &ha_queue->prefix, &nrt.ll_addr,
					ha_info_pref, ha_info_lifetime);
		} else {
			if (mipv6_halist_delete(&ha_queue->prefix) < 0) {
				DEBUG(DBG_INFO, "mipv6_ra_rcv: Not able "
				      "to delete %x:%x:%x:%x:%x:%x:%x:%x",
				      NIPV6ADDR(&ha_queue->prefix));
			}
		}
		kfree(ha_queue);
		ha_queue = tmp;
	}
	return mipv6_router_event(&nrt);
}

/**********************************************************************
 *
 * MIPv6 Module Init / Cleanup
 *
 **********************************************************************/

#ifdef CONFIG_SYSCTL
/* Sysctl table */

extern int __max_rtr_reach_time;
extern int eager_cell_switching;

int max_reach = 1000;
int min_reach = 1;
int max_eagercell = 1;
int min_eagercell = 0;

extern int 
mipv6_mdetect_mech_sysctl(ctl_table *, int, struct file *, void *, size_t *);

extern int 
mipv6_router_reach_sysctl(ctl_table *, int, struct file *, void *, size_t *);

extern int 
mipv6_max_tnls_sysctl(ctl_table *, int, struct file *, void *, size_t *);

extern int 
mipv6_min_tnls_sysctl(ctl_table *, int, struct file *, void *, size_t *);


ctl_table mipv6_mobility_table[] = {
	{NET_IPV6_MOBILITY_DEBUG, "debuglevel", &mipv6_debug, sizeof(int),
	 0644, NULL, &proc_dointvec},
	{NET_IPV6_MOBILITY_AUTH, "use_auth", &mipv6_use_auth, sizeof(int),
	 0644, NULL, &proc_dointvec},

	{NET_IPV6_MOBILITY_ROUTER_REACH, "max_router_reachable_time",
	 &__max_rtr_reach_time, sizeof(int), 0644, NULL,
	 &proc_dointvec_minmax, &sysctl_intvec, 0, &min_reach, &max_reach},

	{NET_IPV6_MOBILITY_MDETECT_MECHANISM, "eager_cell_switching",
	 &eager_cell_switching, sizeof(int), 0644, NULL,
	 &proc_dointvec_minmax, &sysctl_intvec, 0, &min_eagercell, 
	 &max_eagercell},

	{NET_IPV6_MOBILITY_MAX_TNLS, "max_tnls", &mipv6_max_tnls, sizeof(int),
	 0644, NULL, &mipv6_max_tnls_sysctl},
	{NET_IPV6_MOBILITY_MIN_TNLS, "min_tnls", &mipv6_min_tnls, sizeof(int),
	 0644, NULL, &mipv6_min_tnls_sysctl},
	{0}
};
ctl_table mipv6_table[] = {
	{NET_IPV6_MOBILITY, "mobility", NULL, 0, 0555, mipv6_mobility_table},
	{0}
};

static struct ctl_table_header *mipv6_sysctl_header;
static struct ctl_table mipv6_net_table[];
static struct ctl_table mipv6_root_table[];

ctl_table mipv6_net_table[] = {
	{NET_IPV6, "ipv6", NULL, 0, 0555, mipv6_table},
	{0}
};

ctl_table mipv6_root_table[] = {
	{CTL_NET, "net", NULL, 0, 0555, mipv6_net_table},
	{0}
};
#endif /* CONFIG_SYSCTL */

extern void mipv6_rr_init(void);

/*  Initialize the module  */
int __init init_module(void)
{
	int err = 0;

	printk(KERN_INFO "MIPL Mobile IPv6 for Linux Mobile Node %s (%s)\n",
	       MIPLVERSION, MIPV6VERSION);
	mipv6_cap = CAP_CN | CAP_MN;

#ifdef CONFIG_IPV6_MOBILITY_DEBUG
	printk(KERN_INFO "Debug-level: %d\n", mipv6_debug);
#endif
/* common init */
	if ((err = mipv6_bcache_init(MIPV6_BCACHE_SIZE)) < 0)
		goto bcache_fail;

	if ((err = mipv6_stats_init()) < 0)
		goto stats_fail;
	mipv6_rr_init();

#ifdef CONFIG_SYSCTL
	mipv6_sysctl_header = register_sysctl_table(mipv6_root_table, 0);
#endif

	if ((err = mipv6_mh_common_init()) < 0)
		goto mh_fail;

	mipv6_mh_mn_init();

	MIPV6_SETCALL(mipv6_modify_txoptions, mipv6_modify_txoptions);

	MIPV6_SETCALL(mipv6_handle_homeaddr, mipv6_handle_homeaddr);
	MIPV6_SETCALL(mipv6_icmp_handle_homeaddr, mipv6_icmp_handle_homeaddr);
/* end of common */

	mipv6_initialize_tunnel();

	if ((err = mipv6_mn_init()) < 0)
		goto mn_fail;

	MIPV6_SETCALL(mipv6_ra_rcv, mipv6_mn_ra_rcv);

	if ((err = mipv6_halist_init(MIPV6_HALIST_SIZE)) < 0)
		goto halist_fail;

	mipv6_initialize_pfx_icmpv6();

	if ((err = mipv6_initialize_icmpv6()) < 0)
		goto icmpv6_fail;

	if ((err = mipv6_ioctl_mn_init()) < 0)
		goto ioctl_fail;

	mipv6_is_initialized = 1;

	return 0;

ioctl_fail:
	mipv6_shutdown_icmpv6();
icmpv6_fail:
	mipv6_shutdown_pfx_icmpv6();
	mipv6_halist_exit();
halist_fail:
	mipv6_mn_exit();
mn_fail:
	mipv6_shutdown_tunnel();
	mipv6_invalidate_calls();
	mipv6_mh_common_exit();
mh_fail:
#ifdef CONFIG_SYSCTL
	unregister_sysctl_table(mipv6_sysctl_header);
#endif
	mipv6_stats_exit();
stats_fail:
	mipv6_bcache_exit();
bcache_fail:
	return err;
}

/*  Cleanup module  */
void __exit cleanup_module(void)
{
	printk(KERN_INFO "mobile_ip6.o exiting.\n");
	mipv6_ioctl_mn_exit();
	/* Invalidate all custom kernel hooks.  No need to do this
           separately for all hooks. */
	mipv6_invalidate_calls();
	mipv6_halist_exit();
	mipv6_shutdown_icmpv6();
	mipv6_shutdown_pfx_icmpv6();
	mipv6_mn_exit();
	mipv6_shutdown_tunnel();

/* common cleanup */
#ifdef CONFIG_SYSCTL
	unregister_sysctl_table(mipv6_sysctl_header);
#endif
	mipv6_mh_common_exit();
	mipv6_stats_exit();
	mipv6_bcache_exit();
}
