/*
 * INET		An implementation of the TCP/IP protocol suite for the LINUX
 *		operating system.  INET is implemented using the  BSD Socket
 *		interface as the means of communication with the user level.
 *
 *		Generic frame diversion
 *
 * Authors:	
 * 		Benoit LOCHER:	initial integration within the kernel with support for ethernet
 * 		Dave Miller:	improvement on the code (correctness, performance and source files)
 *
 */
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/sched.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/socket.h>
#include <linux/in.h>
#include <linux/inet.h>
#include <linux/ip.h>
#include <linux/udp.h>
#include <linux/netdevice.h>
#include <linux/etherdevice.h>
#include <linux/skbuff.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <net/dst.h>
#include <net/arp.h>
#include <net/sock.h>
#include <net/ipv6.h>
#include <net/ip.h>
#include <asm/uaccess.h>
#include <asm/system.h>
#include <asm/checksum.h>
#include <linux/divert.h>
#include <linux/sockios.h>

const char sysctl_divert_version[32]="0.46";	/* Current version */

static int __init dv_init(void)
{
	return 0;
}
module_init(dv_init);

/*
 * Allocate a divert_blk for a device. This must be an ethernet nic.
 */
int alloc_divert_blk(struct net_device *dev)
{
	int alloc_size = (sizeof(struct divert_blk) + 3) & ~3;

	dev->divert = NULL;
	if (dev->type == ARPHRD_ETHER) {
		dev->divert = (struct divert_blk *)
			kmalloc(alloc_size, GFP_KERNEL);
		if (dev->divert == NULL) {
			printk(KERN_INFO "divert: unable to allocate divert_blk for %s\n",
			       dev->name);
			return -ENOMEM;
		}

		memset(dev->divert, 0, sizeof(struct divert_blk));
		dev_hold(dev);
	}

	return 0;
} 

/*
 * Free a divert_blk allocated by the above function, if it was 
 * allocated on that device.
 */
void free_divert_blk(struct net_device *dev)
{
	if (dev->divert) {
		kfree(dev->divert);
		dev->divert=NULL;
		dev_put(dev);
	}
}

/*
 * Adds a tcp/udp (source or dest) port to an array
 */
static int add_port(u16 ports[], u16 port)
{
	int i;

	if (port == 0)
		return -EINVAL;

	/* Storing directly in network format for performance,
	 * thanks Dave :)
	 */
	port = htons(port);

	for (i = 0; i < MAX_DIVERT_PORTS; i++) {
		if (ports[i] == port)
			return -EALREADY;
	}
	
	for (i = 0; i < MAX_DIVERT_PORTS; i++) {
		if (ports[i] == 0) {
			ports[i] = port;
			return 0;
		}
	}

	return -ENOBUFS;
}

/*
 * Removes a port from an array tcp/udp (source or dest)
 */
static int remove_port(u16 ports[], u16 port)
{
	int i;

	if (port == 0)
		return -EINVAL;
	
	/* Storing directly in network format for performance,
	 * thanks Dave !
	 */
	port = htons(port);

	for (i = 0; i < MAX_DIVERT_PORTS; i++) {
		if (ports[i] == port) {
			ports[i] = 0;
			return 0;
		}
	}

	return -EINVAL;
}

/* Some basic sanity checks on the arguments passed to divert_ioctl() */
static int check_args(struct divert_cf *div_cf, struct net_device **dev)
{
	char devname[32];
	int ret;

	if (dev == NULL)
		return -EFAULT;
	
	/* GETVERSION: all other args are unused */
	if (div_cf->cmd == DIVCMD_GETVERSION)
		return 0;
	
	/* Network device index should reasonably be between 0 and 1000 :) */
	if (div_cf->dev_index < 0 || div_cf->dev_index > 1000) 
		return -EINVAL;
			
	/* Let's try to find the ifname */
	sprintf(devname, "eth%d", div_cf->dev_index);
	*dev = dev_get_by_name(devname);
	
	/* dev should NOT be null */
	if (*dev == NULL)
		return -EINVAL;

	ret = 0;

	/* user issuing the ioctl must be a super one :) */
	if (!capable(CAP_SYS_ADMIN)) {
		ret = -EPERM;
		goto out;
	}

	/* Device must have a divert_blk member NOT null */
	if ((*dev)->divert == NULL)
		ret = -EINVAL;
out:
	dev_put(*dev);
	return ret;
}

/*
 * control function of the diverter
 */
#if 0
#define	DVDBG(a)	\
	printk(KERN_DEBUG "divert_ioctl() line %d %s\n", __LINE__, (a))
#else
#define	DVDBG(a)
#endif

int divert_ioctl(unsigned int cmd, struct divert_cf __user *arg)
{
	struct divert_cf	div_cf;
	struct divert_blk	*div_blk;
	struct net_device	*dev;
	int			ret;

	switch (cmd) {
	case SIOCGIFDIVERT:
		DVDBG("SIOCGIFDIVERT, copy_from_user");
		if (copy_from_user(&div_cf, arg, sizeof(struct divert_cf)))
			return -EFAULT;
		DVDBG("before check_args");
		ret = check_args(&div_cf, &dev);
		if (ret)
			return ret;
		DVDBG("after checkargs");
		div_blk = dev->divert;
			
		DVDBG("befre switch()");
		switch (div_cf.cmd) {
		case DIVCMD_GETSTATUS:
			/* Now, just give the user the raw divert block
			 * for him to play with :)
			 */
			if (copy_to_user(div_cf.arg1.ptr, dev->divert,
					 sizeof(struct divert_blk)))
				return -EFAULT;
			break;

		case DIVCMD_GETVERSION:
			DVDBG("GETVERSION: checking ptr");
			if (div_cf.arg1.ptr == NULL)
				return -EINVAL;
			DVDBG("GETVERSION: copying data to userland");
			if (copy_to_user(div_cf.arg1.ptr,
					 sysctl_divert_version, 32))
				return -EFAULT;
			DVDBG("GETVERSION: data copied");
			break;

		default:
			return -EINVAL;
		}

		break;

	case SIOCSIFDIVERT:
		if (copy_from_user(&div_cf, arg, sizeof(struct divert_cf)))
			return -EFAULT;

		ret = check_args(&div_cf, &dev);
		if (ret)
			return ret;

		div_blk = dev->divert;

		switch(div_cf.cmd) {
		case DIVCMD_RESET:
			div_blk->divert = 0;
			div_blk->protos = DIVERT_PROTO_NONE;
			memset(div_blk->tcp_dst, 0,
			       MAX_DIVERT_PORTS * sizeof(u16));
			memset(div_blk->tcp_src, 0,
			       MAX_DIVERT_PORTS * sizeof(u16));
			memset(div_blk->udp_dst, 0,
			       MAX_DIVERT_PORTS * sizeof(u16));
			memset(div_blk->udp_src, 0,
			       MAX_DIVERT_PORTS * sizeof(u16));
			return 0;
				
		case DIVCMD_DIVERT:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ENABLE:
				if (div_blk->divert)
					return -EALREADY;
				div_blk->divert = 1;
				break;

			case DIVARG1_DISABLE:
				if (!div_blk->divert)
					return -EALREADY;
				div_blk->divert = 0;
				break;

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_IP:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ENABLE:
				if (div_blk->protos & DIVERT_PROTO_IP)
					return -EALREADY;
				div_blk->protos |= DIVERT_PROTO_IP;
				break;

			case DIVARG1_DISABLE:
				if (!(div_blk->protos & DIVERT_PROTO_IP))
					return -EALREADY;
				div_blk->protos &= ~DIVERT_PROTO_IP;
				break;

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_TCP:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ENABLE:
				if (div_blk->protos & DIVERT_PROTO_TCP)
					return -EALREADY;
				div_blk->protos |= DIVERT_PROTO_TCP;
				break;

			case DIVARG1_DISABLE:
				if (!(div_blk->protos & DIVERT_PROTO_TCP))
					return -EALREADY;
				div_blk->protos &= ~DIVERT_PROTO_TCP;
				break;

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_TCPDST:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ADD:
				return add_port(div_blk->tcp_dst,
						div_cf.arg2.uint16);
				
			case DIVARG1_REMOVE:
				return remove_port(div_blk->tcp_dst,
						   div_cf.arg2.uint16);

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_TCPSRC:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ADD:
				return add_port(div_blk->tcp_src,
						div_cf.arg2.uint16);

			case DIVARG1_REMOVE:
				return remove_port(div_blk->tcp_src,
						   div_cf.arg2.uint16);

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_UDP:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ENABLE:
				if (div_blk->protos & DIVERT_PROTO_UDP)
					return -EALREADY;
				div_blk->protos |= DIVERT_PROTO_UDP;
				break;

			case DIVARG1_DISABLE:
				if (!(div_blk->protos & DIVERT_PROTO_UDP))
					return -EALREADY;
				div_blk->protos &= ~DIVERT_PROTO_UDP;
				break;

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_UDPDST:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ADD:
				return add_port(div_blk->udp_dst,
						div_cf.arg2.uint16);

			case DIVARG1_REMOVE:
				return remove_port(div_blk->udp_dst,
						   div_cf.arg2.uint16);

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_UDPSRC:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ADD:
				return add_port(div_blk->udp_src,
						div_cf.arg2.uint16);

			case DIVARG1_REMOVE:
				return remove_port(div_blk->udp_src,
						   div_cf.arg2.uint16);

			default:
				return -EINVAL;
			}

			break;

		case DIVCMD_ICMP:
			switch(div_cf.arg1.int32) {
			case DIVARG1_ENABLE:
				if (div_blk->protos & DIVERT_PROTO_ICMP)
					return -EALREADY;
				div_blk->protos |= DIVERT_PROTO_ICMP;
				break;

			case DIVARG1_DISABLE:
				if (!(div_blk->protos & DIVERT_PROTO_ICMP))
					return -EALREADY;
				div_blk->protos &= ~DIVERT_PROTO_ICMP;
				break;

			default:
				return -EINVAL;
			}

			break;

		default:
			return -EINVAL;
		}

		break;

	default:
		return -EINVAL;
	}

	return 0;
}


/*
 * Check if packet should have its dest mac address set to the box itself
 * for diversion
 */

#define	ETH_DIVERT_FRAME(skb) \
	memcpy(eth_hdr(skb), skb->dev->dev_addr, ETH_ALEN); \
	skb->pkt_type=PACKET_HOST
		
void divert_frame(struct sk_buff *skb)
{
	struct ethhdr			*eth = eth_hdr(skb);
	struct iphdr			*iph;
	struct tcphdr			*tcph;
	struct udphdr			*udph;
	struct divert_blk		*divert = skb->dev->divert;
	int				i, src, dst;
	unsigned char			*skb_data_end = skb->data + skb->len;

	/* Packet is already aimed at us, return */
	if (!memcmp(eth, skb->dev->dev_addr, ETH_ALEN))
		return;
	
	/* proto is not IP, do nothing */
	if (eth->h_proto != htons(ETH_P_IP))
		return;
	
	/* Divert all IP frames ? */
	if (divert->protos & DIVERT_PROTO_IP) {
		ETH_DIVERT_FRAME(skb);
		return;
	}
	
	/* Check for possible (maliciously) malformed IP frame (thanks Dave) */
	iph = (struct iphdr *) skb->data;
	if (((iph->ihl<<2)+(unsigned char*)(iph)) >= skb_data_end) {
		printk(KERN_INFO "divert: malformed IP packet !\n");
		return;
	}

	switch (iph->protocol) {
	/* Divert all ICMP frames ? */
	case IPPROTO_ICMP:
		if (divert->protos & DIVERT_PROTO_ICMP) {
			ETH_DIVERT_FRAME(skb);
			return;
		}
		break;

	/* Divert all TCP frames ? */
	case IPPROTO_TCP:
		if (divert->protos & DIVERT_PROTO_TCP) {
			ETH_DIVERT_FRAME(skb);
			return;
		}

		/* Check for possible (maliciously) malformed IP
		 * frame (thanx Dave)
		 */
		tcph = (struct tcphdr *)
			(((unsigned char *)iph) + (iph->ihl<<2));
		if (((unsigned char *)(tcph+1)) >= skb_data_end) {
			printk(KERN_INFO "divert: malformed TCP packet !\n");
			return;
		}

		/* Divert some tcp dst/src ports only ?*/
		for (i = 0; i < MAX_DIVERT_PORTS; i++) {
			dst = divert->tcp_dst[i];
			src = divert->tcp_src[i];
			if ((dst && dst == tcph->dest) ||
			    (src && src == tcph->source)) {
				ETH_DIVERT_FRAME(skb);
				return;
			}
		}
		break;

	/* Divert all UDP frames ? */
	case IPPROTO_UDP:
		if (divert->protos & DIVERT_PROTO_UDP) {
			ETH_DIVERT_FRAME(skb);
			return;
		}

		/* Check for possible (maliciously) malformed IP
		 * packet (thanks Dave)
		 */
		udph = (struct udphdr *)
			(((unsigned char *)iph) + (iph->ihl<<2));
		if (((unsigned char *)(udph+1)) >= skb_data_end) {
			printk(KERN_INFO
			       "divert: malformed UDP packet !\n");
			return;
		}

		/* Divert some udp dst/src ports only ? */
		for (i = 0; i < MAX_DIVERT_PORTS; i++) {
			dst = divert->udp_dst[i];
			src = divert->udp_src[i];
			if ((dst && dst == udph->dest) ||
			    (src && src == udph->source)) {
				ETH_DIVERT_FRAME(skb);
				return;
			}
		}
		break;
	}
}