/*
  This is a module implements port knocking for iptables.
  This file is distributed under the terms of the GNU General Public
  License (GPL). Copies of the GPL can be obtained from:
  ftp://prep.ai.mit.edu/pub/gnu/GPL

  Borrowed some functions from xt_connlimit.c
  2008-01-05 Hernani Guerra and
  2008-01-05 Angelo Arrifano <miknix@gmail.com> : Initial development
*/

#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/jhash.h>
#include <linux/tcp.h>
#include <linux/ip.h>

#include <linux/netfilter/x_tables.h>
#include <linux/netfilter_ipv4/ipt_portkng.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Angelo Arrifano <miknix@gmail.com>");

/* Hash table for ip tracking */
static struct portkng_ipnfo {
	__be32 saddr;
	__u16 knocks;
	bool allow;
	/* ___ tstamp; */
	struct portkng_ipnfo *next;
	struct portkng_ipnfo *prev;
} *portkng_hash[256];

/* Hash function */
static u_int32_t portkng_rnd;
static bool portkng_rnd_inited;

static inline __u32
portkng_iphash(__be32 addr)
{
	if (unlikely(!portkng_rnd_inited)) {
		get_random_bytes(&portkng_rnd, sizeof(portkng_rnd));
		portkng_rnd_inited = true;
	}
	return jhash_1word((__force __u32)addr, portkng_rnd) & 0xFF;
}

/* IP lookup function */
static struct portkng_ipnfo *
portkng_ipnfo_lookup(__be32 addr, __u32 *hash)
{
	struct portkng_ipnfo *ptr;
	*hash = portkng_iphash(addr);
	ptr = portkng_hash[*hash];

	while (ptr) {
		if (ptr->saddr == addr)
			return ptr;
		ptr = ptr->next;
	}
	return NULL;
}

static void
portkng_ipnfo_add(__be32 addr, __u32 hash)
{
	struct portkng_ipnfo *ptr;

	/* Check if ipnfo list exists */
	if (!portkng_hash[hash]) {
		/* Create ipnfo list head */
		/* FIXME: Check for NOMEM */
		ptr = kzalloc(sizeof(struct portkng_ipnfo), GFP_ATOMIC);
		ptr->saddr = addr;
		ptr->knocks = 1;
		ptr->allow = false;
		ptr->next = NULL;
		ptr->prev = NULL;
		portkng_hash[hash] = ptr;
	} else {
		/* Seek for list tail */
		ptr = portkng_hash[hash];
		while (ptr->next)
			ptr = ptr->next;

		/* Link ip to list */
		/* FIXME: Check for NOMEM */
		ptr->next = kzalloc(sizeof(struct portkng_ipnfo), GFP_ATOMIC);
		ptr->next->saddr = addr;
		ptr->next->knocks = 1;
		ptr->next->allow = false;
		ptr->next->next = NULL;
		ptr->next->prev = ptr;
	}
}

static void
portkng_ipnfo_del(struct portkng_ipnfo *ipnfo, __u32 hash)
{
	struct portkng_ipnfo *ptr = ipnfo;

	/* Is this list head? */
	if(!ptr->prev){
		kfree(ptr);
		portkng_hash[hash] = NULL;
	}else{
		ptr = ptr->prev;
		kfree(ptr->next);
		ptr->next = NULL;
	}
}


static bool
match(const struct sk_buff *skb,
      const struct net_device *in,
      const struct net_device *out,
      const struct xt_match *match,
      const void *matchinfo,
      int offset,
      unsigned int protoff,
      bool *hotdrop)
{
	const struct ipt_portkng_info *info = matchinfo;   /* match info for rule */
	const struct iphdr *iph = (struct iphdr *)ip_hdr(skb);
	const struct tcphdr *tcph = (struct tcphdr *)(skb_network_header(skb) +
	                                            (iph->ihl * 4));
	struct portkng_ipnfo *ipnfo;
	__u32 hash;

	/* Lookup src ip information */
	if (!(ipnfo = portkng_ipnfo_lookup(iph->saddr, &hash))) {
		/* new ip, is first knock correct? */
		if (tcph->syn && ntohs(tcph->dest) == info->ports[0]) {
			printk("Knock[0]: Correct.\n");
			/* yes, add ip information to list */
			portkng_ipnfo_add(iph->saddr, hash);
		}
		return(false);
	}

	if(ipnfo->allow){
		/* Session is granted for this src ip */
		if(tcph->fin){
			ipnfo->allow = false;
			printk("Knock[x]: Closed.\n");
		}
		return(true);
	}

	if(tcph->syn){
		/* knocking at correct port? */
		if (ntohs(tcph->dest) == info->ports[ipnfo->knocks]) {
			printk("Knock[%d]: Correct.\n", ipnfo->knocks);
			/* right port, is this the last knock? */
			if (ipnfo->knocks == info->count - 1) {
				printk("Knock[x]: Opened.\n");
				ipnfo->allow = true;
			}
			/* not the last, increment port knocks	*/
			ipnfo->knocks++;
		} else {
			printk("Knock[%d]: WRONG.\n", ipnfo->knocks);
			/* Ooops! Wrong door. Delete ip info. */
			portkng_ipnfo_del(ipnfo, hash);
		}
	}

	return(false);
}

static bool
checkentry(const char *tablename,
	   const void *ip,
	   const struct xt_match *match,
	   void *matchinfo,
	   unsigned int hook_mask)
{
	const struct ipt_portkng_info *info = matchinfo;   /* match info for rule */

	/* everything ok. trust user.. */
		return 1;
}

static struct xt_match portkng_match = { 
	.name = "portkng",
	.revision = 0,
	.family = AF_INET,
	.match = match,
	.matchsize = sizeof(struct ipt_portkng_info),
	.checkentry = checkentry,
	.me = THIS_MODULE
};

static int __init init(void)
{
	return xt_register_match(&portkng_match);
}

static void __exit fini(void)
{
	xt_unregister_match(&portkng_match);
}

module_init(init);
module_exit(fini);
