/**
 * Generic icmp routines
 *
 * Authors:
 * Jaakko Laine <medved@iki.fi>
 *
 * $Id: s.mipv6_icmp.c 1.23 02/11/25 11:18:16+02:00 vnuorval@amber.hut.mediapoli.com $
 *
 * 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/icmpv6.h>
#include <net/checksum.h>
#include <net/ipv6.h>
#include <net/ip6_route.h>
#include <net/mipv6.h>
#include <net/mipglue.h>

#include "debug.h"
#include "util.h"
#include "bcache.h"
#include "mipv6_icmp.h"

struct icmpv6_msg {
	struct icmp6hdr icmph;
	__u8 *data;
};

#define MIPV6_ICMP_HOP_LIMIT 64

static struct socket *icmpv6_socket = NULL;
static __u16 identifier = 0;

/**
 * mipv6_icmpv6_dest_unreach - Destination Unreachable ICMP error message handler
 * @skb: buffer containing ICMP error message
 *
 * Special Mobile IPv6 ICMP handling.  If Correspondent Node receives
 * persistent ICMP Destination Unreachable messages for a destination
 * in its Binding Cache, the binding should be deleted.  See draft
 * section 8.8.
 **/
static int mipv6_icmpv6_rcv_dest_unreach(struct sk_buff *skb)
{
	struct icmp6hdr *phdr = (struct icmp6hdr *) skb->h.raw;
	int left = (skb->tail - (unsigned char *) (phdr + 1))
	    - sizeof(struct ipv6hdr);
	struct ipv6hdr *hdr = (struct ipv6hdr *) (phdr + 1);
	struct ipv6_opt_hdr *ehdr;
	struct ipv6_rt_hdr *rthdr = NULL;
	struct in6_addr *addr, *ha;
	int hdrlen, nexthdr = hdr->nexthdr;

	DEBUG_FUNC();

	ehdr = (struct ipv6_opt_hdr *) (hdr + 1);

	while (left > 0) {
		hdrlen = ipv6_optlen(ehdr);
		if (hdrlen > left)
			return 0;

		if (!(nexthdr == NEXTHDR_HOP || nexthdr == NEXTHDR_DEST || 
		      nexthdr == NEXTHDR_ROUTING))
			return 0;

		if (nexthdr == NEXTHDR_ROUTING) {
			rthdr = (struct ipv6_rt_hdr *) ehdr;
			if (rthdr->segments_left != 1)
				return 0;
			if (rthdr->type == IPV6_SRCRT_TYPE_2)
				break;
		}
		nexthdr = ehdr->nexthdr;
		ehdr = (struct ipv6_opt_hdr *) ((u8 *) ehdr + hdrlen);
		left -= hdrlen;
	}

	if (rthdr == NULL) return 0;

	addr = (struct in6_addr *) ((u32 *) rthdr + 2);
	ha = (struct in6_addr *) &skb->nh.ipv6h->daddr;
	if (mipv6_bcache_exists(addr, ha) >= 0) {
		if (!mipv6_bcache_delete(addr, ha, CACHE_ENTRY)) {
			DEBUG(DBG_INFO, "Deleted bcache entry "
			      "%x:%x:%x:%x:%x:%x:%x:%x "
			      "%x:%x:%x:%x:%x:%x:%x:%x (reason: "
			      "dest unreachable) ",
			      NIPV6ADDR(addr), NIPV6ADDR(ha));
		}
	}
	return 0;
}

/**
 * mipv6_icmpv6_send - generic icmpv6 message send
 * @daddr: destination address
 * @saddr: source address
 * @type: icmp type
 * @code: icmp code
 * @id: packet identifier. If null, uses internal counter to get new id
 * @data: packet data
 * @datalen: length of data in bytes
 */
void mipv6_icmpv6_send(struct in6_addr *daddr, struct in6_addr *saddr, int type,
		       int code, __u16 *id, void *data, int datalen)
{
	int len, err;
	struct sock *sk = icmpv6_socket->sk;
	struct sk_buff *skb;
	struct ipv6hdr *hdr;
	struct icmpv6_msg *msg;
	struct flowi fl;
	struct dst_entry *dst;

	DEBUG_FUNC();

	if (!daddr)
		return;

	fl.proto = IPPROTO_ICMPV6;
	fl.fl6_dst = daddr;
	fl.fl6_src = saddr;
	fl.fl6_flowlabel = 0;
	fl.uli_u.icmpt.type = type;
	fl.uli_u.icmpt.code = code;

	dst = ip6_route_output(NULL, &fl);

	if (dst->error || !dst->dev)
		return;

	len = sizeof(struct icmp6hdr) + datalen;
	skb = sock_alloc_send_skb(sk, MAX_HEADER + len +
				  dst->dev->hard_header_len + 15, 0, &err);

	if (!skb) {
		DEBUG(DBG_WARNING, "Alloc skb failed");
		return;
	}

	skb->dst = dst;
	skb->dev = dst->dev;

	/* ll header */
	if (!skb->dev->hard_header)
		goto fail;

	skb_reserve(skb, (skb->dev->hard_header_len + 15) & ~15);

	if (skb->dev->hard_header(skb, skb->dev, ETH_P_IPV6,
				  skb->dst->neighbour->ha,
				  NULL, len) < 0)
		goto fail;

	/* IP */
	skb->protocol = htons(ETH_P_IPV6);

	hdr = (struct ipv6hdr *) skb_put(skb, sizeof(struct ipv6hdr));
	skb->nh.ipv6h = hdr;
	*(u32 *)hdr = htonl(0x60000000);

	hdr->payload_len = htons(len);
	hdr->nexthdr = IPPROTO_ICMPV6;
	hdr->hop_limit = MIPV6_ICMP_HOP_LIMIT;

	ipv6_addr_copy(&hdr->saddr, saddr);
	ipv6_addr_copy(&hdr->daddr, daddr);

	/* icmp */
	msg = (struct icmpv6_msg *) skb_put(skb, sizeof(struct icmp6hdr));
	msg->icmph.icmp6_type = type;
	msg->icmph.icmp6_code = code;
	msg->icmph.icmp6_cksum = 0;

	if (id)
		msg->icmph.icmp6_identifier = htons(*id);
	else
		msg->icmph.icmp6_identifier = htons(identifier++);

	/* data */
	if (datalen > 0) {
		msg->data = skb_put(skb, datalen);
		memcpy(msg->data, data, datalen);
	}

	msg->icmph.icmp6_cksum = csum_ipv6_magic(&skb->nh.ipv6h->saddr,
						 daddr, len, 
						 IPPROTO_ICMPV6,
						 csum_partial((__u8 *) msg, 
							      len, 0));

	/* pojehali! */
	dev_queue_xmit(skb);

	ICMP6_INC_STATS_BH(Icmp6OutMsgs);

	return;

 fail:
	kfree_skb(skb);
}

/**
 * icmp6_rcv - ICMPv6 receive and multiplex
 * @skb: buffer containing ICMP message
 *
 * Generic ICMPv6 receive function to multiplex messages to approriate
 * handlers.  Only used for ICMP messages with special handling in
 * Mobile IPv6.
 **/
static void icmp6_rcv(struct sk_buff *skb)
{
	struct icmp6hdr *hdr = (struct icmp6hdr *) skb->h.raw;
	switch (hdr->icmp6_type) {
	case ICMPV6_DEST_UNREACH:
		mipv6_icmpv6_rcv_dest_unreach(skb);
		break;

	case ICMPV6_PARAMPROB:
		mipv6_icmpv6_rcv_paramprob(skb);
		break;

	case MIPV6_DHAAD_REPLY:
		mipv6_icmpv6_rcv_dhaad_rep(skb);
		break;

	case MIPV6_PREFIX_ADV:
		mipv6_icmpv6_rcv_pfx_adv(skb);
		break;

	case MIPV6_DHAAD_REQUEST:
		mipv6_icmpv6_rcv_dhaad_req(skb);
		break;

	case MIPV6_PREFIX_SOLICIT:
		mipv6_icmpv6_rcv_pfx_sol(skb);
		break;
	}
}

int mipv6_initialize_icmpv6(void)
{
	struct sock *sk;
	int err;

	if ((icmpv6_socket = sock_alloc()) == NULL) {
		DEBUG(DBG_ERROR, "Cannot allocate icmpv6_socket");
		return -1;
	}
	icmpv6_socket->type = SOCK_RAW;

	if ((err = sock_create(PF_INET6, SOCK_RAW, IPPROTO_ICMP, 
			       &icmpv6_socket)) < 0) {
		DEBUG(DBG_ERROR, "Cannot initialize icmpv6_socket");
		sock_release(icmpv6_socket);
		icmpv6_socket = NULL; /* For safety */
		return err;
	}
	sk = icmpv6_socket->sk;
	sk->allocation = GFP_ATOMIC;
	sk->prot->unhash(sk);

	/* Register our ICMP handler */
	MIPV6_SETCALL(mipv6_icmp_rcv, icmp6_rcv);
	return 0;
}

void mipv6_shutdown_icmpv6(void)
{

	if (icmpv6_socket)
		sock_release(icmpv6_socket);
	icmpv6_socket = NULL; /* For safety */
}
