/*
 * General IPsec4 related functions
 *
 * Copyright (C) 2003 SuSE Linux AG
 *
 * Written by okir@suse.de
 */

#include <linux/config.h>
#include <linux/version.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h> /* kmalloc() */
#include <linux/errno.h>  /* error codes */
#include <linux/types.h>  /* size_t */
#include <linux/interrupt.h> /* mark_bh */

#include <linux/netdevice.h>   /* struct device, and other headers */
#include <linux/etherdevice.h> /* eth_type_trans */
#include <linux/ip.h>          /* struct iphdr */
#include <linux/skbuff.h>
#include <asm/uaccess.h>

#include <linux/in6.h>
#include <asm/checksum.h>
#include <net/ip.h>
#include <net/udp.h>
#include <net/tcp.h>

#include <linux/pfkeyv2.h>
#include <linux/pfkey.h>
#include <linux/ipsec.h>
#include <net/sadb.h>
#include <net/spd.h>

#include "ipsec_tunnel.h"
#include "ipsec4.h"

#define AH_AMAX	32
#define ESP_IVMAX 32

/*
 * Compute the AH
 * Unfortunately, cryptoapi's hmac functions can hash only contiguous
 * regions of memory, so we need to do copy everything to a temporary
 * buffer:-/
 *
 * When we get here, the SA referring to this algorithm info must
 * be read locked, the skb must be linear, and the AH must have been
 * initialized and pushed.
 */
int
ipsec4_compute_ah(struct auth_algo_info *alg, struct sk_buff *skb,
		  unsigned int offset,
		  unsigned char *result, size_t res_size)
{
	struct ip_auth_hdr *ah = (struct ip_auth_hdr *) skb->h.raw;
	struct iphdr *iph = skb->nh.iph;
	unsigned char *buffer, *ptr;
	unsigned char hash[AH_AMAX];
	int ah_len, total;
	int err;

	ah_len = 8 + (ah->ah_hl << 2);

	/* Note - AH should include 12 bytes of HMAC for MD5 and SHA1
	 * as per RFC 240{2,3}.
	 * If the AH includes more than that, it _might_ be the rest
	 * of the digest, but it might as well be just padding. */
	if (!alg || !alg->dx || !alg->dx->di
	 || sizeof(hash) < alg->dx->di->blocksize
	 || res_size < alg->digest_len
	 || ah_len < AH_HDRLEN + alg->digest_len)
		return -EINVAL;

	/* This is not quite right - RFC2402 explicitly says options
	 * should be checksummed too. */
	total = sizeof(struct iphdr) + ah_len + skb->len - offset;
	buffer = (unsigned char *) kmalloc(total, GFP_ATOMIC);
	if (!buffer)
		return -ENOMEM;

	ptr = buffer;

	memcpy(ptr, iph, sizeof(*iph));
	iph = (struct iphdr *) ptr;
	ptr += sizeof(*iph);
	iph->ihl      = 5;
	iph->tos      = 0;
	iph->frag_off = 0;
	iph->ttl      = 0;
	iph->check    = 0;

	memcpy(ptr, ah, AH_HDRLEN);
	memset(ptr + AH_HDRLEN, 0, ah_len - AH_HDRLEN);
	ptr += ah_len;

	if ((err = skb_copy_bits(skb, offset, ptr, skb->len - offset)) != 0) {
		kfree(buffer);
		return err;
	}

	alg->dx->di->hmac_atomic(alg->dx, alg->key, alg->key_len,
			buffer, total, hash);

	memset(buffer, 0, total);
	kfree(buffer);

	memcpy(result, hash, alg->digest_len);
	return alg->digest_len;
}

static inline void
sockaddr_setv4(struct sockaddr_storage *ss, __u32 addr, __u16 port)
{
	struct sockaddr_in *sin = (struct sockaddr_in *) ss;

	memset(ss, 0, sizeof(*ss));
	sin->sin_family = AF_INET;
	sin->sin_addr.s_addr = addr;
	sin->sin_port = port;
}

/*
 * Send ACQUIRE message for a given skb
 */
int
ipsec4_acquire(struct ipsec_sp *sp, struct sk_buff *skb)
{
	struct iphdr *iph = skb->nh.iph;
	struct ipsec_sa *sa;
	__u16 sport = 0, dport = 0;
	void *nexthdr;

	if (!(sa = ipsec_sa_kmalloc()))
		return -ENOMEM;

	sa->ipsec_proto = SADB_SATYPE_ESP;

#if 0
	if (ipsec_sa_copy_ident(&sa->src_ident, &sp->src_ident) < 0
	 || ipsec_sa_copy_ident(&sa->dst_ident, &sp->dst_ident) < 0)
	 	goto out;
#endif

	nexthdr = ((char *) iph) + (iph->ihl << 2);
	if (iph->protocol == IPPROTO_UDP) {
		sport = ((struct udphdr *) nexthdr)->source;
		dport = ((struct udphdr *) nexthdr)->dest;
	} else {
		sport = ((struct tcphdr *) nexthdr)->source;
		dport = ((struct tcphdr *) nexthdr)->dest;
	}

	sockaddr_setv4(&sa->src, iph->saddr, sport);
	sa->prefixlen_s = 32;
	sockaddr_setv4(&sa->dst, iph->daddr, dport);
	sa->prefixlen_d = 32;

	ipsec_sa_acquire(sa);
	ipsec_sa_put(sa);
	return 0;
}

/*
 * Given an sa_index (which is what a policy is referring to)
 * try to look up the SA corresponding to it, store it in the
 * sa_index and bump the refcount.
 */
struct ipsec_sa *
ipsec4_policy_bind(struct ipsec_sp *policy, struct sa_index **saidp)
{
	struct ipsec_sa *found = NULL;
	struct sa_index *said;
	int have_write_lock = 0;

	read_lock_bh(&policy->lock);
	while ((said = *saidp) != NULL) {
		/* We may have cached an SA that has died.
		 * Detach and try again */
		if ((found = said->sa) != NULL) {
			if (found->state != SADB_SASTATE_DEAD)
				break;
			if (have_write_lock) {
				ipsec_sa_put(found);
				said->sa = NULL;
			}
		}

		if (!have_write_lock) {
			/* Drop read lock, obtain write lock, and
			 * start all over again.
			 */
			read_unlock_bh(&policy->lock);
			write_lock_bh(&policy->lock);
			have_write_lock = 1;
		} else {
			/* policy is write locked, and said->sa is NULL */
			said->sa = sadb_find_by_sa_index(said);
		}
	}

	ipsec_sa_hold(found);
	if (have_write_lock)
		write_unlock_bh(&policy->lock);
	else
		read_unlock_bh(&policy->lock);

	return found;
}

/*
 * Get the list of SAs referenced by a policy, in the order applied on
 * output.
 */
int
ipsec4_policy_to_salist(struct ipsec_sp *policy, struct ipsec_sa **result, unsigned int max)
{
	struct ipsec_sa *list[MAX_CHAINED_SAS+1];
	struct ipsec_sa *sa, *next = NULL;
	unsigned int i, count = 0;

	if (policy->ipip_sa_idx) {
		if (!(sa = ipsec4_policy_bind(policy, &policy->ipip_sa_idx)))
			goto failed;
		list[count++] = sa;
	}
	if (policy->comp_sa_idx) {
		if (!(sa = ipsec4_policy_bind(policy, &policy->comp_sa_idx)))
			goto failed;
		list[count++] = sa;
	}
	if (policy->esp_sa_idx) {
		if (!(sa = ipsec4_policy_bind(policy, &policy->esp_sa_idx)))
			goto failed;
		list[count++] = sa;
	}
	if (policy->auth_sa_idx) {
		if (!(sa = ipsec4_policy_bind(policy, &policy->auth_sa_idx)))
			goto failed;
		list[count++] = sa;
	}

	for (i = 0, next = NULL; i < count; i++) {
		if (count > MAX_CHAINED_SAS)
			goto toomany;

		sa = list[i];

		/* If the preceding SA had a forward pointer, make
		 * sure it matches what we extracted from the policy */
		if (next && sa != next)
			goto failed;

		read_lock_bh(&sa->lock);
		if ((next = sa->onext) != NULL) {
			if (i + 1 == count) {
				list[count++] = next;
				ipsec_sa_hold(next);
			}
		}
		read_unlock_bh(&sa->lock);
	}

	if (count == 0) {
		if (net_ratelimit())
			printk(KERN_NOTICE "No SAs associated with policy\n");
		goto failed;
	}
	if (count > max) 
		goto toomany;

	memcpy(result, list, count * sizeof(struct ipsec_sa *));
	return count;

toomany:if (net_ratelimit())
		printk(KERN_NOTICE "Too many chained SAs\n");

failed:	while (count--)
		ipsec_sa_put(list[count]);
	return -EINVAL;
}

/*
 * Look up policy corresponding to IPv4 header
 */
struct ipsec_sp *
ipsec4_policy_lookup(struct iphdr *iph, int proto)
{
	struct selector matcher;

	memset(&matcher, 0, sizeof(matcher));
	matcher.mode = (proto == IPPROTO_IPIP)? IPSEC_MODE_TUNNEL : IPSEC_MODE_TRANSPORT;
	matcher.proto = AF_INET;

	sockaddr_setv4(&matcher.src, iph->saddr, 0);
	matcher.prefixlen_s = 32;

	sockaddr_setv4(&matcher.dst, iph->daddr, 0);
	matcher.prefixlen_d = 32;

	return ipsec_sp_get(&matcher);
}

/*
 * Create a new shunt (e.g. for a HOLD route)
 */
int
ipsec4_new_shunt(struct iphdr *iph, int action, struct ipsec_sp **result)
{
	struct selector matcher;
	struct ipsec_sp *policy;
	int err;

	memset(&matcher, 0, sizeof(matcher));
	matcher.mode = IPSEC_MODE_TRANSPORT;
	matcher.proto = 0;

	sockaddr_setv4(&matcher.src, iph->saddr, 0);
	matcher.prefixlen_s = 32;

	sockaddr_setv4(&matcher.dst, iph->daddr, 0);
	matcher.prefixlen_d = 32;

	err = spd_find_by_selector(&matcher, &policy, 1);
	if (err == -ESRCH) {
		policy->policy_action = action;
		*result = policy;
		err = 0;
	} else {
		if (policy)
			ipsec_sp_put(policy);
	}

	return err;
}

/*
 * Debugging: dump an IPv4 header
 * Stolen from FreeSWAN
 */
#ifdef CONFIG_IPSEC_DEBUG
void
ipsec4_print_ip(const char *prefix, struct iphdr *ip)
{
	struct tcphdr *tcph = NULL;
	struct udphdr *udph = NULL;
	struct icmphdr *icmph = NULL;
	const char *pname = NULL;
	u16 sport = 0, dport = 0;

	switch (ip->protocol) {
	case IPPROTO_UDP:
		udph = (struct udphdr*)((caddr_t)ip + (ip->ihl << 2));
		pname = " (UDP)";
		sport = udph->source;
		dport = udph->dest;
		break;
	case IPPROTO_TCP:
		tcph = (struct tcphdr*)((caddr_t)ip + (ip->ihl << 2));
		pname = " (TCP)";
		sport = tcph->source;
		dport = tcph->dest;
		break;
	case IPPROTO_ICMP:
		icmph = (struct icmphdr *) ((caddr_t)ip + (ip->ihl << 2));
		pname = " (ICMP)";
		break;
	case IPPROTO_IPIP:
		pname = " (IPIP)";
		break;
	case IPPROTO_IP:
		pname = " (IP)";
		break;
	case IPPROTO_ESP:
		pname = " (ESP)";
		break;
	case IPPROTO_AH:
		pname = " (AH)";
		break;
	case IPPROTO_COMP:
		pname = " (COMP)";
		break;
	default:
		pname = "";
	}

	printk(KERN_INFO "%s:   IP:", prefix);
	printk(" ihl:%d", ip->ihl*4);
	printk(" ver:%d", ip->version);
	printk(" tos:%d", ip->tos);
	printk(" tlen:%d", ntohs(ip->tot_len));
	printk(" id:%d", ntohs(ip->id));
	printk(" %s%s%sfrag_off:%d",
               ip->frag_off & __constant_htons(IP_CE) ? "CE " : "",
               ip->frag_off & __constant_htons(IP_DF) ? "DF " : "",
               ip->frag_off & __constant_htons(IP_MF) ? "MF " : "",
               (ntohs(ip->frag_off) & IP_OFFSET) << 3);
	printk(" ttl:%d", ip->ttl);
	printk(" proto:%d%s chk:%d saddr:%u.%u.%u.%u",
		ip->protocol, pname,
		ntohs(ip->check),
		NIPQUAD(ip->saddr));

	if (sport)
		printk(":%d", ntohs(sport));

	printk(" daddr:%u.%u.%u.%u",  NIPQUAD(ip->daddr));
	if (dport)
		printk(":%d", ntohs(dport));

	if (icmph)
		printk(" type:code=%d:%d", icmph->type, icmph->code);
	printk("\n");

	if (sysctl_ipsec_debug_ipv4 > 1) {
		__u8 *c;
		int i;
		
		c = ((__u8*)ip) + ip->ihl*4;
		for(i = 0; i < ntohs(ip->tot_len) - ip->ihl*4; i++ /*, c++*/) {
			if(!(i % 16)) {
				if (i)
					printk("\n");
				printk(KERN_INFO
				       "%s:   @%03x:",
				       prefix, i);
			}
			printk(" %02x", c[i]);
		}
		if (i)
			printk("\n");
	}
}

char *
ipsec4_print_sa(struct ipsec_sa *sa, char *buf, size_t buflen)
{
	const char *pname = "";

	switch (sa->ipsec_proto) {
	case SADB_SATYPE_ESP:
		pname = "/ESP"; break;
	case SADB_SATYPE_AH:
		pname = "/AH"; break;
	case SADB_X_SATYPE_IPIP:
		pname = "/IPIP"; break;
	case SADB_X_SATYPE_COMP:
		pname = "/COMP"; break;
	}

	snprintf(buf, buflen, "spi=0x%x, dst=%u.%u.%u.%u, proto=%u%s",
		ntohl(sa->spi),
		NIPQUAD(((struct sockaddr_in *) &sa->src)->sin_addr.s_addr),
		sa->ipsec_proto, pname);
	return buf;
}
#endif /* CONFIG_IPSEC_DEBUG */
