#include <linux/module.h>
#include <linux/timer.h>
#include <linux/skbuff.h>
#include <linux/spinlock.h>
#include <linux/jhash.h>
#include <linux/vmalloc.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/icmp.h>
#include <linux/version.h>
#include <linux/netfilter/x_tables.h>
#include <linux/inet.h>
#include <linux/proc_fs.h>
#include <net/tcp.h>
#include "compat.h"
#include "xt_NAT.h"

#define FLAG_REPLIED   (1 << 0) /* 000001 */
#define FLAG_TCP_FIN   (1 << 1) /* 000010 */

#define TCP_SYN_ACK 0x12
#define TCP_FIN_RST 0x05

static LIST_HEAD(usock_list);
static int sndbuf = 1310720;
static int engine_id = 0;
static unsigned int pdu_data_records = 0;
static unsigned int pdu_seq = 0;
struct netflow5_pdu pdu;

static DEFINE_SPINLOCK(nfsend_lock);

static atomic64_t sessions_active = ATOMIC_INIT(0);
static atomic64_t users_active = ATOMIC_INIT(0);
static atomic64_t sessions_tried = ATOMIC_INIT(0);
static atomic64_t sessions_created = ATOMIC_INIT(0);
static atomic64_t dnat_dropped = ATOMIC_INIT(0);
static atomic64_t frags = ATOMIC_INIT(0);
static atomic64_t related_icmp = ATOMIC_INIT(0);

static char nat_pool_buf[128] = "127.0.0.1-127.0.0.1";
static char *nat_pool = nat_pool_buf;
module_param(nat_pool, charp, 0444);
MODULE_PARM_DESC(nat_pool, "NAT pool range (addr_start-addr_end), default = 127.0.0.1-127.0.0.1");

static int nat_hash_size = 256 * 1024;
module_param(nat_hash_size, int, 0444);
MODULE_PARM_DESC(nat_hash_size, "nat hash size, default = 256k");

static int users_hash_size = 4096;
module_param(users_hash_size, int, 0444);
MODULE_PARM_DESC(users_hash_size, "users hash size, default = 4k");

static char nf_dest_buf[128] = "";
static char *nf_dest = nf_dest_buf;
module_param(nf_dest, charp, 0444);
MODULE_PARM_DESC(nf_dest, "Netflow v5 collectors (addr1:port1[,addr2:port2]), default = none");

u_int32_t nat_htable_vector = 0;
u_int32_t users_htable_vector = 0;

static spinlock_t *create_session_lock;

static DEFINE_SPINLOCK(sessions_timer_lock);
static DEFINE_SPINLOCK(users_timer_lock);
static struct timer_list sessions_cleanup_timer, users_cleanup_timer, nf_send_timer;

struct proc_dir_entry *proc_net_nat;

struct netflow_sock {
    struct list_head list;
    struct socket *sock;
    struct sockaddr_storage addr;   // destination
};

struct xt_nat_htable {
    uint8_t use;
    spinlock_t lock;
    struct hlist_head session;
};

struct nat_htable_ent {
    struct rcu_head rcu;
    struct hlist_node list_node;
    uint8_t  proto;
    uint32_t addr;
    uint16_t port;
    struct nat_session *data;
};

struct nat_session {
    uint32_t in_addr;
    uint16_t in_port;
    uint16_t out_port;
    int16_t  timeout;
    uint8_t  flags;
};

struct xt_users_htable {
    uint8_t use;
    spinlock_t lock;
    struct hlist_head user;
};

struct user_htable_ent {
    struct rcu_head rcu;
    struct hlist_node list_node;
    uint32_t addr;
    uint16_t tcp_count;
    uint16_t udp_count;
    uint16_t other_count;
    uint8_t idle;
};

struct xt_users_htable *ht_users;

static u_int32_t nat_pool_start;
static u_int32_t nat_pool_end;

struct xt_nat_htable *ht_inner, *ht_outer;

static char *print_sockaddr(const struct sockaddr_storage *ss)
{
    static char buf[64];
    snprintf(buf, sizeof(buf), "%pISpc", ss);
    return buf;
}

static inline long timer_end(struct timespec start_time)
{
    struct timespec end_time;
    getrawmonotonic(&end_time);
    return(end_time.tv_nsec - start_time.tv_nsec);
}

static inline struct timespec timer_start(void)
{
    struct timespec start_time;
    getrawmonotonic(&start_time);
    return start_time;
}

static inline u_int32_t
get_pool_size(void)
{
    return ntohl(nat_pool_end)-ntohl(nat_pool_start)+1;
}

static inline u_int32_t
get_nat_addr(const u_int32_t addr)
{
    return htonl(ntohl(nat_pool_start)+reciprocal_scale(jhash_1word(addr, 0), get_pool_size()));
}

static inline u_int32_t
get_hash_nat_ent(const uint8_t proto, const u_int32_t addr, const uint16_t port)
{
    return reciprocal_scale(jhash_3words((u32)proto, addr, (u32)port, 0), nat_hash_size);
}

static inline u_int32_t
get_hash_user_ent(const u_int32_t addr)
{
    return reciprocal_scale(jhash_1word(addr, 0), users_hash_size);
}

static inline u_int32_t pool_table_create(void)
{
    unsigned int sz; /* (bytes) */
    unsigned int pool_size;
    int i;

    pool_size = get_pool_size();

    sz = sizeof(spinlock_t) * pool_size;
    create_session_lock = kzalloc(sz, GFP_KERNEL);

    if (create_session_lock == NULL)
        return -ENOMEM;

    for (i = 0; i < pool_size; i++) {
        spin_lock_init(&create_session_lock[i]);
    }

    printk(KERN_INFO "xt_NAT DEBUG: nat pool table mem: %d\n", sz);

    return 0;
}

void pool_table_remove(void)
{
    kfree(create_session_lock);

    printk(KERN_INFO "xt_NAT pool_table_remove DEBUG: removed\n");
}


static int users_htable_create(void)
{
    unsigned int sz; /* (bytes) */
    int i;

    sz = sizeof(struct xt_users_htable) * users_hash_size;
    ht_users = kzalloc(sz, GFP_KERNEL);

    if (ht_users == NULL)
        return -ENOMEM;

    for (i = 0; i < users_hash_size; i++) {
        spin_lock_init(&ht_users[i].lock);
        INIT_HLIST_HEAD(&ht_users[i].user);
        ht_users[i].use = 0;
    }

    printk(KERN_INFO "xt_NAT DEBUG: users htable mem: %d\n", sz);
    return 0;
}

void users_htable_remove(void)
{
    struct user_htable_ent *user;
    struct hlist_head *head;
    struct hlist_node *next;
    int i;

    for (i = 0; i < users_hash_size; i++) {
        spin_lock_bh(&ht_users[i].lock);
        head = &ht_users[i].user;
        hlist_for_each_entry_safe(user, next, head, list_node) {
            hlist_del_rcu(&user->list_node); 
            ht_users[i].use--;
            kfree_rcu(user, rcu);
        }

        if (ht_users[i].use != 0) {
            printk(KERN_WARNING "xt_NAT users_htable_remove ERROR: bad use value: %d in element %d\n", ht_users[i].use, i);
        }
        spin_unlock_bh(&ht_users[i].lock);
    }
    kfree(ht_users);
    printk(KERN_INFO "xt_NAT users_htable_remove DONE\n");
    return;
}

void nat_htable_remove(void)
{
    struct nat_htable_ent *session;
    struct hlist_head *head;
    struct hlist_node *next;
    unsigned int i;
    void *p;

    for (i = 0; i < nat_hash_size; i++) {
        spin_lock_bh(&ht_inner[i].lock);
        head = &ht_inner[i].session;
        hlist_for_each_entry_safe(session, next, head, list_node) {
            hlist_del_rcu(&session->list_node);
            ht_inner[i].use--;
            kfree_rcu(session, rcu);
        }
        if (ht_inner[i].use != 0) {
            printk(KERN_WARNING "xt_NAT nat_htable_remove inner ERROR: bad use value: %d in element %d\n", ht_inner[i].use, i);
        }
        spin_unlock_bh(&ht_inner[i].lock);
    }

    for (i = 0; i < nat_hash_size; i++) {
        spin_lock_bh(&ht_outer[i].lock);
        head = &ht_outer[i].session;
        hlist_for_each_entry_safe(session, next, head, list_node) {
            hlist_del_rcu(&session->list_node);
            ht_outer[i].use--;
            p = session->data;
            kfree_rcu(session, rcu);
            kfree(p);
        }
        if (ht_outer[i].use != 0) {
            printk(KERN_WARNING "xt_NAT nat_htable_remove outer ERROR: bad use value: %d in element %d\n", ht_outer[i].use, i);
        }
        spin_unlock_bh(&ht_outer[i].lock);
    }
    printk(KERN_INFO "xt_NAT nat_htable_remove DONE\n");
    return;
}


static int nat_htable_create(void)
{
    unsigned int sz; /* (bytes) */
    int i;

    sz = sizeof(struct xt_nat_htable) * nat_hash_size;
    ht_inner = kzalloc(sz, GFP_KERNEL);
    if (ht_inner == NULL)
        return -ENOMEM;

    for (i = 0; i < nat_hash_size; i++) {
        spin_lock_init(&ht_inner[i].lock);
        INIT_HLIST_HEAD(&ht_inner[i].session);
        ht_inner[i].use = 0;
    }

    printk(KERN_INFO "xt_NAT DEBUG: sessions htable inner mem: %d\n", sz);


    ht_outer = kzalloc(sz, GFP_KERNEL);
    if (ht_outer == NULL)
        return -ENOMEM;

    for (i = 0; i < nat_hash_size; i++) {
        spin_lock_init(&ht_outer[i].lock);
        INIT_HLIST_HEAD(&ht_outer[i].session);
        ht_outer[i].use = 0;
    }

    printk(KERN_INFO "xt_NAT DEBUG: sessions htable outer mem: %d\n", sz);
    return 0;
}

struct nat_htable_ent *lookup_session(struct xt_nat_htable *ht, const uint8_t proto, const u_int32_t addr, const uint16_t port)
{
    struct nat_htable_ent *session;
    struct hlist_head *head;
    unsigned int hash;

    hash = get_hash_nat_ent(proto, addr, port);
    if (ht[hash].use == 0)
        return NULL;

    head = &ht[hash].session;
    hlist_for_each_entry_rcu(session, head, list_node) {
        if (session->addr == addr && session->port == port && session->proto == proto && session->data->timeout > 0) {
            return session;
        } else {
            //printk(KERN_DEBUG "xt_NAT lookup_session miss: %d - %pI4:%d\n", session->proto, &session->addr, ntohs(session->port));
        }
    }
    return NULL;
}

static uint16_t search_free_l4_port(const uint8_t proto, const u_int32_t nataddr, const uint16_t userport)
{
    uint16_t i, freeport;
    for(i = 0; i < 64512; i++) {
        freeport = ntohs(userport) + i;

        if (freeport < 1024) {
            freeport += 1024;
        }

        //printk(KERN_DEBUG "xt_NAT search_free_l4_port: check nat port = %d\n", freeport);

        if(!lookup_session(ht_outer, proto, nataddr, htons(freeport))) {
            return htons(freeport);
        }
    }
    return 0;
}

static int check_user_limits(const u_int8_t proto, const u_int32_t addr)
{
    struct user_htable_ent *user;
    struct hlist_head *head;
    unsigned int hash, is_found, ret;
    unsigned int sessions, session_limit;

    hash = get_hash_user_ent(addr);
    rcu_read_lock_bh();
    head = &ht_users[hash].user;
    is_found=0;
    hlist_for_each_entry_rcu(user, head, list_node) {
        if (user->addr == addr && user->idle < 15) {
            //printk(KERN_DEBUG "xt_NAT check_user_limits hit: %pI4\n", &user->addr);
            if (proto == IPPROTO_TCP) {
                sessions = user->tcp_count;
                session_limit = 4096;
            } else if (proto == IPPROTO_UDP) {
                sessions = user->udp_count;
                session_limit = 4096;
            } else {
                sessions = user->other_count;
                session_limit = 4096;
            }
            is_found=1;
            break;
        } else {
            //printk(KERN_DEBUG "xt_NAT check_user_limits miss: %pI4\n", &user->addr);
        }
    }

    ret=1;
    if (is_found==1) {
        //printk(KERN_DEBUG "xt_NAT check_user_limits: sessions = %d of %d\n", sessions, session_limit);
        if (sessions < session_limit) {
            ret=1;
        } else {
            ret=0;
        }
    } else {
        //printk(KERN_DEBUG "xt_NAT check_user_limits is not found: %pI4\n", &addr);
        ret=1;
    }
    rcu_read_unlock_bh();
    return ret;
}

void update_user_limits(const u_int8_t proto, const u_int32_t addr, const int8_t operation)
{
    struct user_htable_ent *user;
    struct hlist_head *head;
    unsigned int hash, is_found;
    unsigned int sz;
    //u_int32_t nataddr;

    hash = get_hash_user_ent(addr);
    spin_lock_bh(&ht_users[hash].lock);
    head = &ht_users[hash].user;
    is_found=0;
    hlist_for_each_entry(user, head, list_node) {
        if (user->addr == addr && user->idle < 15) {
            //printk(KERN_DEBUG "xt_NAT check_user_limits hit: %pI4\n", &user->addr);
            is_found=1;
            break;
        } else {
            //printk(KERN_DEBUG "xt_NAT check_user_limits miss: %pI4\n", &user->addr);
        }
    }

    if (likely(is_found==1)) {
        user->idle = 0;
        if (proto == IPPROTO_TCP) {
            user->tcp_count += operation;
        } else if (proto == IPPROTO_UDP) {
            user->udp_count += operation;
        } else {
            user->other_count += operation;
        }
    } else {
        //printk(KERN_DEBUG "xt_NAT update_user_limits is not found: %pI4\n", &addr);

        //printk(KERN_DEBUG "xt_NAT update_user_limits: add user_session entry to htable\n");
        sz = sizeof(struct user_htable_ent);
        user = kzalloc(sz, GFP_ATOMIC);

        if (user == NULL) {
            printk(KERN_WARNING "xt_NAT update_user_limits ERROR: Cannot allocate memory for user_session\n");
            spin_unlock_bh(&ht_users[hash].lock);
            return;
        }

        user->addr = addr;
        user->tcp_count = 0;
        user->udp_count = 0;
        user->other_count = 0;
        user->idle = 0;

        if (proto == IPPROTO_TCP) {
            user->tcp_count += operation;
        } else if (proto == IPPROTO_UDP) {
            user->udp_count += operation;
        } else {
            user->other_count += operation;
        }
        hlist_add_head_rcu(&user->list_node, &ht_users[hash].user);
        ht_users[hash].use++;
        atomic64_inc(&users_active);

        //nataddr = get_nat_addr(user->addr);
        //printk(KERN_DEBUG "xt_NAT NEW: %pI4 -> %pI4\n", &user->addr, &nataddr);
    }

    spin_unlock_bh(&ht_users[hash].lock);
    return;
}

/* socket code */
static void sk_error_report(struct sock *sk)
{
    /* clear connection refused errors if any */
    sk->sk_err = 0;

    return;
}

static struct socket *usock_open_sock(const struct sockaddr_storage *addr, void *user_data)
{
    struct socket *sock;
    int error;

    if ((error = sock_create_kern(addr->ss_family, SOCK_DGRAM, IPPROTO_UDP, &sock)) < 0) {
        printk(KERN_WARNING "xt_NAT NEL: sock_create_kern error %d\n", -error);
        return NULL;
    }
    sock->sk->sk_allocation = GFP_ATOMIC;
    sock->sk->sk_prot->unhash(sock->sk); /* hidden from input */
    sock->sk->sk_error_report = &sk_error_report; /* clear ECONNREFUSED */
    sock->sk->sk_user_data = user_data; /* usock */

    if (sndbuf < SOCK_MIN_SNDBUF)
	sndbuf = SOCK_MIN_SNDBUF;

    if (sndbuf)
        sock->sk->sk_sndbuf = sndbuf;
    else
        sndbuf = sock->sk->sk_sndbuf;
    error = sock->ops->connect(sock, (struct sockaddr *)addr, sizeof(*addr), 0);
    if (error < 0) {
        printk(KERN_WARNING "xt_NAT NEL: error connecting UDP socket %d,"
               " don't worry, will try reconnect later.\n", -error);
        /* ENETUNREACH when no interfaces */
        sock_release(sock);
        return NULL;
    }
    return sock;
}

static void netflow_sendmsg(void *buffer, const int len)
{
    struct msghdr msg = { .msg_flags = MSG_DONTWAIT|MSG_NOSIGNAL };
    struct kvec iov = { buffer, len };
    struct netflow_sock *usock;
    int ret;

    //printk(KERN_DEBUG "xt_NAT NEL: Netflow exporting function\n");

    list_for_each_entry(usock, &usock_list, list) {
        //printk(KERN_DEBUG "xt_NAT NEL: Exporting PDU to collector N\n");
        if (!usock->sock)
            usock->sock = usock_open_sock(&usock->addr, usock);

        if (!usock->sock)
            continue;

        ret = kernel_sendmsg(usock->sock, &msg, &iov, 1, (size_t)len);
        if (ret == -EINVAL) {
            if (usock->sock)
                sock_release(usock->sock);
            usock->sock = NULL;
        } else if (ret == -EAGAIN) {
            printk(KERN_WARNING "xt_NAT NEL: increase sndbuf!\n");
        }
    }
}

static void netflow_export_pdu_v5(void)
{
    struct timeval tv;
    int pdusize;

    //printk(KERN_DEBUG "xt_NAT NEL: Forming PDU seq %d, %d records\n", pdu_seq, pdu_data_records);

    if (!pdu_data_records)
        return;

    pdu.version		= htons(5);
    pdu.nr_records	= htons(pdu_data_records);
    pdu.ts_uptime	= htonl(jiffies_to_msecs(jiffies));
    do_gettimeofday(&tv);
    pdu.ts_usecs		= htonl(tv.tv_sec);
    pdu.ts_unsecs	= htonl(tv.tv_usec);
    pdu.seq		= htonl(pdu_seq);
    //pdu.v5.eng_type	= 0;
    pdu.eng_id		= (__u8)engine_id;

    pdusize = NETFLOW5_HEADER_SIZE + sizeof(struct netflow5_record) * pdu_data_records;

    netflow_sendmsg(&pdu, pdusize);

    pdu_seq += pdu_data_records;
    pdu_data_records = 0;
}

static void netflow_export_flow_v5(const uint8_t proto, const u_int32_t useraddr, const uint16_t userport, const u_int32_t nataddr, const uint16_t natport, const int flags)
{
    struct netflow5_record *rec;

    spin_lock_bh(&nfsend_lock);

    rec = &pdu.flow[pdu_data_records++];

    /* make V5 flow record */
    rec->s_addr	= useraddr;
    rec->d_addr	= nataddr;
    rec->nexthop	= nataddr;
    rec->i_ifc	= 0;
    rec->o_ifc	= 0;
    rec->nr_packets = 0;
    rec->nr_octets	= 0;
    rec->first_ms	= htonl(jiffies_to_msecs(jiffies));
    rec->last_ms	= htonl(jiffies_to_msecs(jiffies));
    rec->s_port	= userport;
    rec->d_port	= natport;
    //rec->reserved	= 0; /* pdu is always zeroized for v5 in netflow_switch_version */
    if (flags == 0) {
        rec->tcp_flags	= TCP_SYN_ACK;
    } else {
        rec->tcp_flags  = TCP_FIN_RST;
    }
    rec->protocol	= proto;
    rec->tos	= 0;
    rec->s_as	= userport;
    rec->d_as	= natport;
    rec->s_mask	= 0;
    rec->d_mask	= 0;
    //rec->padding	= 0;

    //printk(KERN_DEBUG "xt_NAT NEL: Add flow %pI4:%d (outside %pI4:%d) to PDU\n", &useraddr, ntohs(userport), &nataddr, ntohs(natport));

    if (pdu_data_records == NETFLOW5_RECORDS_MAX)
        netflow_export_pdu_v5();

    spin_unlock_bh(&nfsend_lock);
}

struct nat_htable_ent *create_nat_session(const uint8_t proto, const u_int32_t useraddr, const uint16_t userport, const u_int32_t nataddr)
{
    unsigned int hash;
    struct nat_htable_ent *session, *session2;
    struct nat_session *data_session;
    uint16_t natport;
    unsigned int sz;
    unsigned int nataddr_id;

    atomic64_inc(&sessions_tried);

    if (unlikely(check_user_limits(proto, useraddr) == 0)) {
        printk(KERN_NOTICE "xt_NAT: %pI4 exceed max allowed sessions\n", &useraddr);
        return NULL;
    }

    nataddr_id = ntohl(nataddr) - ntohl(nat_pool_start);
    //printk(KERN_DEBUG "xt_NAT create_nat_session: nataddr_id = %u (%u - %u)\n", nataddr_id, ntohl(nataddr), ntohl(nat_pool_start));
    spin_lock_bh(&create_session_lock[nataddr_id]);

    rcu_read_lock_bh();
    session = lookup_session(ht_inner, proto, useraddr, userport);
    if(unlikely(session)) {
        //printk(KERN_DEBUG "xt_NAT create_nat_session WARN: Race Condition found\n");
        spin_unlock_bh(&create_session_lock[nataddr_id]);
        return lookup_session(ht_outer, proto, nataddr, session->data->out_port); //тут без потери, но с нюансами внутри nat_tg
    }
    rcu_read_unlock_bh();

    if (likely(proto == IPPROTO_TCP || proto == IPPROTO_UDP || proto == IPPROTO_ICMP)) {
        rcu_read_lock_bh();
        natport = search_free_l4_port(proto, nataddr, userport);
        rcu_read_unlock_bh();
        if (natport == 0) {
            printk(KERN_WARNING "xt_NAT create_nat_session ERROR: Not found free nat port for %d %pI4:%u -> %pI4:XXXX\n", proto, &useraddr, userport, &nataddr);
            spin_unlock_bh(&create_session_lock[nataddr_id]);
            return NULL;
        }
    } else {
        natport = userport;
    }

    sz = sizeof(struct nat_session);
    data_session = kzalloc(sz, GFP_ATOMIC);

    if (unlikely(data_session == NULL)) {
        printk(KERN_WARNING "xt_NAT create_nat_session ERROR: Cannot allocate memory for data_session\n");
        spin_unlock_bh(&create_session_lock[nataddr_id]);
        return NULL;
    }

    sz = sizeof(struct nat_htable_ent);
    session = kzalloc(sz, GFP_ATOMIC);

    if (unlikely(session == NULL)) {
        printk(KERN_WARNING "xt_NAT ERROR: Cannot allocate memory for ht_inner session\n");
        kfree(data_session);
        spin_unlock_bh(&create_session_lock[nataddr_id]);
        return NULL;
    }

    sz = sizeof(struct nat_htable_ent);
    session2 = kzalloc(sz, GFP_ATOMIC);

    if (unlikely(session2 == NULL)) {
        printk(KERN_WARNING "xt_NAT ERROR: Cannot allocate memory for ht_outer session\n");
        kfree(data_session);
        kfree(session);
        spin_unlock_bh(&create_session_lock[nataddr_id]);
        return NULL;
    }

    data_session->in_addr = useraddr;
    data_session->in_port = userport;
    data_session->out_port = natport;
    //data_session->timeout = 600;
    data_session->timeout = 30;
    data_session->flags = 0;

    session->proto = proto;
    session->addr = useraddr;
    session->port = userport;
    session->data = data_session;

    session2->proto = proto;
    session2->addr = nataddr;
    session2->port = natport;
    session2->data = data_session;

    hash = get_hash_nat_ent(proto, useraddr, userport);
    spin_lock_bh(&ht_inner[hash].lock);
    hlist_add_head_rcu(&session->list_node, &ht_inner[hash].session);
    ht_inner[hash].use++;
    spin_unlock_bh(&ht_inner[hash].lock);

    hash = get_hash_nat_ent(proto, nataddr, natport);
    spin_lock_bh(&ht_outer[hash].lock);
    hlist_add_head_rcu(&session2->list_node, &ht_outer[hash].session);
    ht_outer[hash].use++;
    spin_unlock_bh(&ht_outer[hash].lock);

    spin_unlock_bh(&create_session_lock[nataddr_id]);

    update_user_limits(proto, useraddr, 1);

    netflow_export_flow_v5(proto, useraddr, userport, nataddr, natport, 0);

    atomic64_inc(&sessions_created);
    atomic64_inc(&sessions_active);
    //printk(KERN_DEBUG "xt_NAT NEW SESSION: %d %pI4:%u -> %pI4:%u\n", session2->proto, &session2->data->in_addr, ntohs(session2->data->in_port), &session2->addr, ntohs(session2->port));
    rcu_read_lock_bh();
    return lookup_session(ht_outer, proto, nataddr, natport);
}

static unsigned int
nat_tg(struct sk_buff *skb, const struct xt_action_param *par)
{
    struct iphdr *ip;
    struct tcphdr *tcp;
    struct udphdr *udp;
    struct icmphdr *icmp;
    struct nat_htable_ent *session;
    uint32_t nat_addr;
    uint16_t nat_port;
    skb_frag_t *frag;
    const struct xt_nat_tginfo *info = par->targinfo;

    if (unlikely(skb->protocol != htons(ETH_P_IP))) {
        printk(KERN_DEBUG "xt_NAT DEBUG: Drop not IP packet\n");
        return NF_DROP;
    }
    if (unlikely(ip_hdrlen(skb) != sizeof(struct iphdr))) {
        printk(KERN_DEBUG "xt_NAT DEBUG: Drop truncated IP packet\n");
        return NF_DROP;
    }

    ip = (struct iphdr *)skb_network_header(skb);

    if (unlikely(ip->frag_off & htons(IP_OFFSET))) {
        printk(KERN_DEBUG "xt_NAT DEBUG: Drop fragmented IP packet\n");
        return NF_DROP;
    }
    if (unlikely(ip->version != 4)) {
        printk(KERN_DEBUG "xt_NAT DEBUG: Drop not IPv4 IP packet\n");
        return NF_DROP;
    }

    if (info->variant == XTNAT_SNAT) {
        nat_addr = get_nat_addr(ip->saddr);
        //printk(KERN_DEBUG "xt_NAT SNAT: tg = SNAT, outer NAT IP = %pI4", &nat_addr);
        //printk(KERN_DEBUG "xt_NAT SNAT: check IPv4 packet with src ip = %pI4 and dst ip = %pI4\n", &ip->saddr, &ip->daddr);

        if (ip->protocol == IPPROTO_TCP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct tcphdr))) {
                printk(KERN_DEBUG "xt_NAT SNAT: Drop truncated TCP packet\n");
                return NF_DROP;
            }
            skb_set_transport_header(skb, ip->ihl * 4);
            tcp = (struct tcphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);

            //printk(KERN_DEBUG "xt_NAT SNAT: TCP packet with src port = %d\n", ntohs(tcp->source));
            rcu_read_lock_bh();
            session = lookup_session(ht_inner, ip->protocol, ip->saddr, tcp->source);
            if (session) {
                //printk(KERN_DEBUG "xt_NAT SNAT: found session for src ip = %pI4 and src port = %d and nat port = %d\n", &ip->saddr, ntohs(tcp->source), ntohs(session->data->out_port));

                csum_replace4(&ip->check, ip->saddr, nat_addr);
                inet_proto_csum_replace4(&tcp->check, skb, ip->saddr, nat_addr, true);
                inet_proto_csum_replace2(&tcp->check, skb, tcp->source, session->data->out_port, true);

                ip->saddr = nat_addr;
                tcp->source = session->data->out_port;

                /*					if (session->data->flags & FLAG_TCP_CLOSED) {
                						session->data->timeout=5;
                					} else if (tcp->rst || tcp->fin) {
                						session->data->flags |= FLAG_TCP_CLOSED;
                						session->data->timeout=5;
                					} else

                */
                if (tcp->fin || tcp->rst) {
                    session->data->timeout=10;
                    session->data->flags |= FLAG_TCP_FIN;
                } else if (session->data->flags & FLAG_TCP_FIN) {
                    session->data->timeout=10;
                    session->data->flags &= ~FLAG_TCP_FIN;
                } else if ((session->data->flags & FLAG_REPLIED) == 0) {
                    session->data->timeout=30;
                } else {
                    session->data->timeout=300;
                }

                /*
                					if ((session->data->flags & FLAG_REPLIED) == 0) {
                                                                session->data->timeout=30;
                                                        } else {
                                                                session->data->timeout=300;
                                                        }
                */

                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                //printk(KERN_DEBUG "xt_NAT SNAT: NOT found session for src ip = %pI4 and src port = %d\n", &ip->saddr, ntohs(tcp->source));

                /*                                      if (!tcp->syn) {
                                                                //printk(KERN_DEBUG "xt_NAT SNAT: SYN flag is not set. Dropping packet\n");
                                                                return NF_DROP;
                                                        }
                */
                session = create_nat_session(ip->protocol, ip->saddr, tcp->source, nat_addr);
                if (session == NULL) {
                    printk(KERN_NOTICE "xt_NAT SNAT: Cannot create new session. Dropping packet\n");
                    return NF_DROP;
                }

                csum_replace4(&ip->check, ip->saddr, session->addr);
                inet_proto_csum_replace4(&tcp->check, skb, ip->saddr, session->addr, true);
                inet_proto_csum_replace2(&tcp->check, skb, session->data->in_port, session->data->out_port, true);
                ip->saddr = session->addr;
                tcp->source = session->data->out_port;
                rcu_read_unlock_bh();
                //return NF_ACCEPT;
            }

        } else if (ip->protocol == IPPROTO_UDP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct udphdr))) {
                printk(KERN_DEBUG "xt_NAT SNAT: Drop truncated UDP packet\n");
                return NF_DROP;
            }

            skb_set_transport_header(skb, ip->ihl * 4);
            udp = (struct udphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);

            //printk(KERN_DEBUG "xt_NAT SNAT: UDP packet with src port = %d\n", ntohs(udp->source));

            rcu_read_lock_bh();
            session = lookup_session(ht_inner, ip->protocol, ip->saddr, udp->source);
            if (session) {
                //printk(KERN_DEBUG "xt_NAT SNAT: found session for src ip = %pI4 and src port = %d and nat port = %d\n", &ip->saddr, ntohs(udp->source), ntohs(session->data->out_port));

                csum_replace4(&ip->check, ip->saddr, nat_addr);
                if (udp->check) {
                    inet_proto_csum_replace4(&udp->check, skb, ip->saddr, nat_addr, true);
                    inet_proto_csum_replace2(&udp->check, skb, udp->source, session->data->out_port, true);
                }

                ip->saddr = nat_addr;
                udp->source = session->data->out_port;

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    session->data->timeout=30;
                } else {
                    session->data->timeout=300;
                }
                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                //printk(KERN_DEBUG "xt_NAT SNAT: NOT found session for src ip = %pI4 and src port = %d\n", &ip->saddr, ntohs(udp->source));

                session = create_nat_session(ip->protocol, ip->saddr, udp->source, nat_addr);
                if (session == NULL) {
                    printk(KERN_NOTICE "xt_NAT SNAT: Cannot create new session. Dropping packet\n");
                    return NF_DROP;
                }

                csum_replace4(&ip->check, ip->saddr, session->addr);
                if (udp->check) {
                    inet_proto_csum_replace4(&udp->check, skb, ip->saddr, session->addr, true);
                    inet_proto_csum_replace2(&udp->check, skb, session->data->in_port, session->data->out_port, true);
                }
                ip->saddr = session->addr;
                udp->source = session->data->out_port;
                rcu_read_unlock_bh();
                //return NF_ACCEPT;
            }
        } else if (ip->protocol == IPPROTO_ICMP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr))) {
                printk(KERN_DEBUG "xt_NAT SNAT: Drop truncated ICMP packet\n");
                return NF_DROP;
            }

            skb_set_transport_header(skb, ip->ihl * 4);
            icmp = (struct icmphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);

            //printk(KERN_DEBUG "xt_NAT SNAT: ICMP packet with type = %d and code = %d\n", icmp->type, icmp->code);

            nat_port = 0;
            if (icmp->type == 0 || icmp->type == 8) {
                nat_port = icmp->un.echo.id;
            } else if (icmp->type == 3 || icmp->type == 4 || icmp->type == 5 || icmp->type == 11 || icmp->type == 12 || icmp->type == 31) {

            }

            rcu_read_lock_bh();
            session = lookup_session(ht_inner, ip->protocol, ip->saddr, nat_port);
            if (session) {
                //printk(KERN_DEBUG "xt_NAT SNAT: found session for src ip = %pI4 and icmp id = %d\n", &ip->saddr, ntohs(nat_port));

                csum_replace4(&ip->check, ip->saddr, nat_addr);

                ip->saddr = nat_addr;

                if (icmp->type == 0 || icmp->type == 8) {
                    inet_proto_csum_replace2(&icmp->checksum, skb, nat_port, session->data->out_port, true);
                    icmp->un.echo.id = session->data->out_port;
                }

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    session->data->timeout=30;
                } else {
                    session->data->timeout=30;
                }
                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                //printk(KERN_DEBUG "xt_NAT SNAT: NOT found session for src ip = %pI4 and icmp id = %d\n",&ip->saddr, ntohs(nat_port));

                session = create_nat_session(ip->protocol, ip->saddr, nat_port, nat_addr);
                if (session == NULL) {
                    printk(KERN_NOTICE "xt_NAT SNAT: Cannot create new session. Dropping packet\n");
                    return NF_DROP;
                }

                csum_replace4(&ip->check, ip->saddr, session->addr);
                ip->saddr = session->addr;

                if (icmp->type == 0 || icmp->type == 8) {
                    inet_proto_csum_replace2(&icmp->checksum, skb, nat_port, session->data->out_port, true);
                    icmp->un.echo.id = session->data->out_port;
                }
                rcu_read_unlock_bh();
                //return NF_ACCEPT;
            }
        } else {
            //skb_set_transport_header(skb, ip->ihl * 4);

            //printk(KERN_DEBUG "xt_NAT SNAT: Generic IP packet\n");

            rcu_read_lock_bh();
            session = lookup_session(ht_inner, ip->protocol, ip->saddr, 0);
            if (session) {
                //printk(KERN_DEBUG "xt_NAT SNAT: found session for src ip = %pI4\n", &ip->saddr);

                csum_replace4(&ip->check, ip->saddr, nat_addr);

                ip->saddr = nat_addr;

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    session->data->timeout=30;
                } else {
                    session->data->timeout=300;
                }
                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                //printk(KERN_DEBUG "xt_NAT SNAT: NOT found session for src ip = %pI4\n",&ip->saddr);

                session = create_nat_session(ip->protocol, ip->saddr, 0, nat_addr);
                if (session == NULL) {
                    printk(KERN_NOTICE "xt_NAT SNAT: Cannot create new session. Dropping packet\n");
                    return NF_DROP;
                }

                csum_replace4(&ip->check, ip->saddr, session->addr);
                ip->saddr = session->addr;
                rcu_read_unlock_bh();
                //return NF_ACCEPT;
            }
        }
    } else if (info->variant == XTNAT_DNAT) {
        //printk(KERN_DEBUG "xt_NAT DNAT: tg = DNAT, outer NAT IP = %pI4", &ip->daddr);
        //printk(KERN_DEBUG "xt_NAT DNAT: check IPv4 packet with src ip = %pI4 and dst nat ip = %pI4\n", &ip->saddr, &ip->daddr);

        if (ip->protocol == IPPROTO_TCP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct tcphdr))) {
                printk(KERN_DEBUG "xt_NAT DNAT: Drop truncated TCP packet\n");
                return NF_DROP;
            }

            skb_set_transport_header(skb, ip->ihl * 4);
            tcp = (struct tcphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);

            if (unlikely(skb_shinfo(skb)->nr_frags > 1 && skb_headlen(skb) == sizeof(struct iphdr))) {
                frag = &skb_shinfo(skb)->frags[0];
                //printk(KERN_DEBUG "xt_NAT DNAT: frag_size = %d (required %lu)\n", frag->size, sizeof(struct tcphdr));
                if (unlikely(frag->size < sizeof(struct tcphdr))) {
                        printk(KERN_DEBUG "xt_NAT DNAT: drop TCP frag_size = %d\n", frag->size);
                        return NF_DROP;
                }
                tcp = (struct tcphdr *)skb_frag_address_safe(frag);
                if (unlikely(tcp == NULL)) {
                        printk(KERN_DEBUG "xt_NAT DNAT: drop fragmented TCP\n");
                        return NF_DROP;
                }
                atomic64_inc(&frags);
            }

            //printk(KERN_DEBUG "xt_NAT DNAT: TCP packet with dst port = %d\n", ntohs(tcp->dest));

            rcu_read_lock_bh();
            session = lookup_session(ht_outer, ip->protocol, ip->daddr, tcp->dest);
            if (likely(session)) {
                //printk(KERN_DEBUG "xt_NAT DNAT: found session for src ip = %pI4 and src port = %d and nat port = %d\n", &session->data->in_addr, ntohs(session->data->in_port), ntohs(tcp->dest));
                csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                inet_proto_csum_replace4(&tcp->check, skb, ip->daddr, session->data->in_addr, true);
                inet_proto_csum_replace2(&tcp->check, skb, tcp->dest, session->data->in_port, true);
                ip->daddr = session->data->in_addr;
                tcp->dest = session->data->in_port;

                if (tcp->fin || tcp->rst) {
                    session->data->timeout=10;
                    session->data->flags |= FLAG_TCP_FIN;
                } else if (session->data->flags & FLAG_TCP_FIN) {
                    session->data->timeout=10;
                    session->data->flags &= ~FLAG_TCP_FIN;
                } else if ((session->data->flags & FLAG_REPLIED) == 0) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                    session->data->timeout=300;
                    session->data->flags |= FLAG_REPLIED;
                }

                /*					if (((session->data->flags & FLAG_TCP_CLOSED) == 0) && (tcp->rst || tcp->fin)) {
                						session->data->flags |= FLAG_TCP_CLOSED;
                						session->data->timeout=5;
                					} else if (((session->data->flags & FLAG_REPLIED) == 0) && (session->data->flags & FLAG_TCP_CLOSED) == 0) {
                						//printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                						session->data->timeout=300;
                						session->data->flags |= FLAG_REPLIED;
                					}
                */
                /*					if ((session->data->flags & FLAG_REPLIED) == 0 && (tcp->rst || tcp->fin)) {
                						session->data->timeout=5;
                					} else if ((session->data->flags & FLAG_REPLIED) == 0) {
                						//printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                						session->data->timeout=300;
                						session->data->flags |= FLAG_REPLIED;
                					}
                */
                /*
                                                        if ((session->data->flags & FLAG_REPLIED) == 0) {
                                                                //printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                                                                session->data->timeout=300;
                                                                session->data->flags |= FLAG_REPLIED;
                                                        }
                */
                //printk(KERN_DEBUG "xt_NAT DNAT: new dst ip = %pI4 and dst port = %d\n", &ip->daddr, ntohs(tcp->dest));
                //printk(KERN_DEBUG "xt_NAT DNAT: new src ip = %pI4 and src port = %d\n", &ip->saddr, ntohs(tcp->source));
                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                atomic64_inc(&dnat_dropped);
                //printk(KERN_DEBUG "xt_NAT DNAT: NOT found session for nat ip = %pI4 and nat port = %d\n", &ip->daddr, ntohs(tcp->dest));
                //return NF_DROP;
            }
        } else if (ip->protocol == IPPROTO_UDP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct udphdr))) {
                printk(KERN_DEBUG "xt_NAT DNAT: Drop truncated UDP packet\n");
                return NF_DROP;
            }

            skb_set_transport_header(skb, ip->ihl * 4);
            udp = (struct udphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);

            if (unlikely(skb_shinfo(skb)->nr_frags > 1 && skb_headlen(skb) == sizeof(struct iphdr))) {
                frag = &skb_shinfo(skb)->frags[0];
                //printk(KERN_DEBUG "xt_NAT DNAT: frag_size = %d (required %lu)\n", frag->size, sizeof(struct udphdr));
                if (unlikely(frag->size < sizeof(struct udphdr))) {
                        printk(KERN_DEBUG "xt_NAT DNAT: drop UDP frag_size = %d\n", frag->size);
                        return NF_DROP;
                }
                udp = (struct udphdr *)skb_frag_address_safe(frag);
                if (unlikely(udp == NULL)) {
                        printk(KERN_DEBUG "xt_NAT DNAT: drop fragmented UDP\n");
                        return NF_DROP;
                }
                atomic64_inc(&frags);
            }

            //printk(KERN_DEBUG "xt_NAT DNAT: UDP packet with dst port = %d\n", ntohs(udp->dest));

            rcu_read_lock_bh();
            session = lookup_session(ht_outer, ip->protocol, ip->daddr, udp->dest);
            if (likely(session)) {
                //printk(KERN_DEBUG "xt_NAT DNAT: found session for src ip = %pI4 and src port = %d and nat port = %d\n", &session->data->in_addr, ntohs(session->data->in_port), ntohs(udp->dest));
                csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                if (udp->check) {
                    inet_proto_csum_replace4(&udp->check, skb, ip->daddr, session->data->in_addr, true);
                    inet_proto_csum_replace2(&udp->check, skb, udp->dest, session->data->in_port, true);
                }
                ip->daddr = session->data->in_addr;
                udp->dest = session->data->in_port;

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                    session->data->timeout=300;
                    session->data->flags |= FLAG_REPLIED;
                }

                //printk(KERN_DEBUG "xt_NAT DNAT: new dst ip = %pI4 and dst port = %d\n", &ip->daddr, ntohs(udp->dest));
                //printk(KERN_DEBUG "xt_NAT DNAT: new src ip = %pI4 and src port = %d\n", &ip->saddr, ntohs(udp->source));
                rcu_read_unlock_bh();
            } else {
                rcu_read_unlock_bh();
                atomic64_inc(&dnat_dropped);
                //printk(KERN_DEBUG "xt_NAT DNAT: NOT found session for nat ip = %pI4 and nat port = %d\n", &ip->daddr, ntohs(udp->dest));
                //return NF_DROP;
            }
        } else if (ip->protocol == IPPROTO_ICMP) {
            if (unlikely(skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr))) {
                printk(KERN_DEBUG "xt_NAT DNAT: Drop truncated ICMP packet\n");
                return NF_DROP;
            }

            skb_set_transport_header(skb, ip->ihl * 4);
            icmp = (struct icmphdr *)skb_transport_header(skb);
            skb_reset_transport_header(skb);
            //printk(KERN_DEBUG "xt_NAT DNAT: ICMP packet with type = %d and code = %d\n", icmp->type, icmp->code);

            nat_port = 0;
            if (icmp->type == 0 || icmp->type == 8) {
                nat_port = icmp->un.echo.id;
            } else if (icmp->type == 3 || icmp->type == 4 || icmp->type == 5 || icmp->type == 11 || icmp->type == 12 || icmp->type == 31) {
                atomic64_inc(&related_icmp);
                //printk(KERN_DEBUG "xt_NAT DNAT: Len: skb=%d, iphdr=%d\n",skb->len, ip_hdrlen(skb));
                if (skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr) + sizeof(struct iphdr)) {
                    printk(KERN_DEBUG "xt_NAT DNAT: Drop related ICMP packet witch truncated IP header\n");
                    return NF_DROP;
                }

                skb_set_network_header(skb,sizeof(struct icmphdr) + sizeof(struct iphdr));
                ip = (struct iphdr *)skb_network_header(skb);
                skb_reset_network_header(skb);

                //printk(KERN_DEBUG "xt_NAT DNAT: Related ICMP\n");
                //printk(KERN_DEBUG "xt_NAT DNAT: Second IP HDR: proto = %d and saddr = %pI4 and daddr = %pI4\n", ip->protocol, &ip->saddr, &ip->daddr);

                if (ip->protocol == IPPROTO_TCP) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Related TCP len: skb=%d, iphdr=%d\n",skb->len, ip_hdrlen(skb));
                    if (skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr) + sizeof(struct iphdr) + 8) {
                        printk(KERN_DEBUG "xt_NAT DNAT: Drop related ICMP packet witch truncated TCP header\n");
                        return NF_DROP;
                    }
                    skb_set_transport_header(skb, (ip->ihl * 4) + sizeof(struct icmphdr) + sizeof(struct iphdr));
                    tcp = (struct tcphdr *)skb_transport_header(skb);
                    skb_reset_transport_header(skb);
                    //port = tcp->source;
                    //printk(KERN_DEBUG "xt_NAT DNAT: TCP packet with source nat port = %d\n", ntohs(tcp->source));
                    rcu_read_lock_bh();
                    session = lookup_session(ht_outer, ip->protocol, ip->saddr, tcp->source);
                    if (session) {
                        csum_replace4(&ip->check, ip->saddr, session->data->in_addr);
                        //inet_proto_csum_replace4(&tcp->check, skb, ip->saddr, session->data->in_addr, true);
                        //inet_proto_csum_replace2(&tcp->check, skb, tcp->source, session->data->in_port, true);
                        ip->saddr = session->data->in_addr;
                        tcp->source = session->data->in_port;
                    } else {
                        rcu_read_unlock_bh();
                        return NF_ACCEPT;
                    }

                    //skb_reset_network_header(skb);
                    ip = (struct iphdr *)skb_network_header(skb);

                    csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                    ip->daddr = session->data->in_addr;
                    rcu_read_unlock_bh();
                } else if (ip->protocol == IPPROTO_UDP) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Related UDP len: skb=%d, iphdr=%d\n",skb->len, ip_hdrlen(skb));
                    if (skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr) + sizeof(struct iphdr) + 8) {
                        printk(KERN_DEBUG "xt_NAT DNAT: Drop related ICMP packet witch truncated UDP header\n");
                        return NF_DROP;
                    }

                    skb_set_transport_header(skb, (ip->ihl * 4) + sizeof(struct icmphdr) + sizeof(struct iphdr));
                    udp = (struct udphdr *)skb_transport_header(skb);
                    skb_reset_transport_header(skb);
                    //printk(KERN_DEBUG "xt_NAT DNAT: UDP packet with source nat port = %d\n", ntohs(udp->source));

                    rcu_read_lock_bh();
                    session = lookup_session(ht_outer, ip->protocol, ip->saddr, udp->source);
                    if (session) {
                        csum_replace4(&ip->check, ip->saddr, session->data->in_addr);
                        //inet_proto_csum_replace4(&tcp->check, skb, ip->saddr, session->data->in_addr, true);
                        //inet_proto_csum_replace2(&tcp->check, skb, tcp->source, session->data->in_port, true);
                        ip->saddr = session->data->in_addr;
                        udp->source = session->data->in_port;
                    } else {
                        rcu_read_unlock_bh();
                        return NF_ACCEPT;
                    }

                    //skb_reset_network_header(skb);
                    ip = (struct iphdr *)skb_network_header(skb);

                    csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                    ip->daddr = session->data->in_addr;
                    rcu_read_unlock_bh();
                } else if (ip->protocol == IPPROTO_ICMP) {
                    if (skb->len < ip_hdrlen(skb) + sizeof(struct icmphdr) + sizeof(struct iphdr) + 8) {
                        printk(KERN_DEBUG "xt_NAT DNAT: Drop related ICMP packet witch truncated ICMP header\n");
                        return NF_DROP;
                    }

                    skb_set_transport_header(skb, (ip->ihl * 4) + sizeof(struct icmphdr) + sizeof(struct iphdr));
                    icmp = (struct icmphdr *)skb_transport_header(skb);
                    skb_reset_transport_header(skb);
                    //printk(KERN_DEBUG "xt_NAT DNAT: ICMP packet\n");

                    nat_port = 0;
                    if (icmp->type == 0 || icmp->type == 8) {
                        nat_port = icmp->un.echo.id;
                    }

                    rcu_read_lock_bh();
                    session = lookup_session(ht_outer, ip->protocol, ip->saddr, nat_port);
                    if (session) {
                        csum_replace4(&ip->check, ip->saddr, session->data->in_addr);
                        //inet_proto_csum_replace4(&tcp->check, skb, ip->saddr, session->data->in_addr, true);
                        //inet_proto_csum_replace2(&tcp->check, skb, tcp->source, session->data->in_port, true);
                        ip->saddr = session->data->in_addr;

                        if (icmp->type == 0 || icmp->type == 8) {
                            inet_proto_csum_replace2(&icmp->checksum, skb, nat_port, session->data->in_port, true);
                            icmp->un.echo.id = session->data->in_port;
                        }

                    } else {
                        rcu_read_unlock_bh();
                        return NF_ACCEPT;
                    }

                    //skb_reset_network_header(skb);
                    ip = (struct iphdr *)skb_network_header(skb);

                    csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                    ip->daddr = session->data->in_addr;
                    rcu_read_unlock_bh();
                }

                return NF_ACCEPT;

            }
            rcu_read_lock_bh();
            session = lookup_session(ht_outer, ip->protocol, ip->daddr, nat_port);
            if (likely(session)) {
                //printk(KERN_DEBUG "xt_NAT DNAT: found session for src ip = %pI4 and icmp id = %d\n", &session->data->in_addr, ntohs(nat_port));
                csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                ip->daddr = session->data->in_addr;

                if (icmp->type == 0 || icmp->type == 8) {
                    inet_proto_csum_replace2(&icmp->checksum, skb, nat_port, session->data->in_port, true);
                    icmp->un.echo.id = session->data->in_port;
                }

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                    session->data->timeout=30;
                    session->data->flags |= FLAG_REPLIED;
                }
                rcu_read_unlock_bh();

                //printk(KERN_DEBUG "xt_NAT DNAT: new dst ip = %pI4 and icmp id = %d\n", &ip->daddr, ntohs(nat_port));
            } else {
                rcu_read_unlock_bh();
                atomic64_inc(&dnat_dropped);
                //printk(KERN_DEBUG "xt_NAT DNAT: NOT found session for nat ip = %pI4 and icmp id = %d\n", &ip->daddr, ntohs(nat_port));
                //return NF_DROP;
            }
        } else {
            //skb_set_transport_header(skb, ip->ihl * 4);
            //printk(KERN_DEBUG "xt_NAT DNAT: Generic IP packet\n");

            nat_port = 0;
            rcu_read_lock_bh();
            session = lookup_session(ht_outer, ip->protocol, ip->daddr, nat_port);
            if (likely(session)) {
                //printk(KERN_DEBUG "xt_NAT DNAT: found session for src ip = %pI4 and icmp id = %d\n", &session->data->in_addr, ntohs(nat_port));
                csum_replace4(&ip->check, ip->daddr, session->data->in_addr);
                ip->daddr = session->data->in_addr;

                if ((session->data->flags & FLAG_REPLIED) == 0) {
                    //printk(KERN_DEBUG "xt_NAT DNAT: Changing state from UNREPLIED to REPLIED\n");
                    session->data->timeout=300;
                    session->data->flags |= FLAG_REPLIED;
                }
                rcu_read_unlock_bh();

                //printk(KERN_DEBUG "xt_NAT DNAT: new dst ip = %pI4\n", &ip->daddr);
            } else {
                rcu_read_unlock_bh();
                atomic64_inc(&dnat_dropped);
                //printk(KERN_DEBUG "xt_NAT DNAT: NOT found session for nat ip = %pI4\n", &ip->daddr);
                //return NF_DROP;
            }
        }
    }

    //printk(KERN_DEBUG "xt_NAT ----------------\n");

    return NF_ACCEPT;
}

void users_cleanup_timer_callback( unsigned long data )
{
    struct user_htable_ent *user;
    struct hlist_head *head;
    struct hlist_node *next;
    unsigned int i;
    u_int32_t vector_start, vector_end;

    spin_lock_bh(&users_timer_lock);

    if (ht_users == NULL) {
        printk(KERN_WARNING "xt_NAT USERS CLEAN ERROR: Found null ptr for ht_users\n");
        spin_unlock_bh(&users_timer_lock);
        return;
    }

    vector_start = users_htable_vector * (users_hash_size/60);
    if (users_htable_vector == 60) {
        vector_end = users_hash_size;
        users_htable_vector = 0;
    } else {
        vector_end = vector_start + (users_hash_size/60);
        users_htable_vector++;
    }

    for (i = vector_start; i < vector_end; i++) {
        spin_lock_bh(&ht_users[i].lock);
        if (ht_users[i].use > 0) {
            head = &ht_users[i].user;
            hlist_for_each_entry_safe(user, next, head, list_node) {
                if (user->tcp_count == 0 && user->udp_count == 0 && user->other_count == 0) {
                    user->idle++;
                }
                if (user->idle > 15) {
                    //printk(KERN_DEBUG "xt_NAT USERS CLEAN ----------------\n");
                    //printk(KERN_DEBUG "xt_NAT USERS CLEAN: Entry for destroy with src ip = %pI4\n", &user->addr);
                    hlist_del_rcu(&user->list_node);
                    ht_users[i].use--;
                    kfree_rcu(user, rcu);
                    atomic64_dec(&users_active);
                    //printk(KERN_DEBUG "xt_NAT USERS CLEAN ----------------\n");
                }
            }
        }
        spin_unlock_bh(&ht_users[i].lock);
    }
    mod_timer( &users_cleanup_timer, jiffies + msecs_to_jiffies(1000) );
    spin_unlock_bh(&users_timer_lock);
}

void sessions_cleanup_timer_callback( unsigned long data )
{
    struct nat_htable_ent *session;
    struct hlist_head *head;
    struct hlist_node *next;
    unsigned int i;
    void *p;
    u_int32_t vector_start, vector_end;

    spin_lock_bh(&sessions_timer_lock);

    //printk( "xt_NAT TIMER CLEAN: called at (%ld)\n", jiffies );

    if (ht_inner == NULL || ht_outer == NULL) {
        printk(KERN_WARNING "xt_NAT SESSIONS CLEAN ERROR: Found null ptr for ht_inner/ht_outer\n");
        spin_unlock_bh(&sessions_timer_lock);
        return;
    }

    vector_start = nat_htable_vector * (nat_hash_size/100);
    if (nat_htable_vector == 100) {
        vector_end = nat_hash_size;
        nat_htable_vector = 0;
    } else {
        vector_end = vector_start + (nat_hash_size/100);
        nat_htable_vector++;
    }

    for (i = vector_start; i < vector_end; i++) {
        spin_lock_bh(&ht_inner[i].lock);
        if (ht_inner[i].use > 0) {
            head = &ht_inner[i].session;
            hlist_for_each_entry_safe(session, next, head, list_node) {
                session->data->timeout -= 10;
                if (session->data->timeout == 0) {
                    netflow_export_flow_v5(session->proto, session->addr, session->port, get_nat_addr(session->addr), session->data->out_port, 1);
                } else if (session->data->timeout <= -10) {
                    hlist_del_rcu(&session->list_node);
                    ht_inner[i].use--;
                    kfree_rcu(session, rcu);
                    update_user_limits(session->proto, session->addr, -1);
                }
            }
        }
        spin_unlock_bh(&ht_inner[i].lock);
    }

    for (i = vector_start; i < vector_end; i++) {
        spin_lock_bh(&ht_outer[i].lock);
        if (ht_outer[i].use > 0) {
            head = &ht_outer[i].session;
            hlist_for_each_entry_safe(session, next, head, list_node) {
                if (session->data->timeout <= -10) {
                    hlist_del_rcu(&session->list_node);
                    ht_outer[i].use--;
                    p = session->data;
                    kfree_rcu(session, rcu);
                    kfree(p);
                    atomic64_dec(&sessions_active);
                }
            }
        }
        spin_unlock_bh(&ht_outer[i].lock);
    }

    mod_timer( &sessions_cleanup_timer, jiffies + msecs_to_jiffies(100) );
    spin_unlock_bh(&sessions_timer_lock);
}

void nf_send_timer_callback( unsigned long data )
{
    spin_lock_bh(&nfsend_lock);
    //printk(KERN_DEBUG "xt_NAT TIMER: Exporting netflow by timer\n");
    netflow_export_pdu_v5();
    mod_timer( &nf_send_timer, jiffies + msecs_to_jiffies(1000) );
    spin_unlock_bh(&nfsend_lock);
}

static int nat_seq_show(struct seq_file *m, void *v)
{
    struct nat_htable_ent *session;
    struct hlist_head *head;
    unsigned int i, count;

    count=0;

    seq_printf(m, "Proto SrcIP:SrcPort -> NatIP:NatPort\n");
    for (i = 0; i < nat_hash_size; i++) {
        rcu_read_lock_bh();
        if (ht_outer[i].use > 0) {
            head = &ht_outer[i].session;
            hlist_for_each_entry_rcu(session, head, list_node) {
                if (session->data->timeout > 0) {
                    seq_printf(m, "%d %pI4:%u -> %pI4:%u --- ttl: %d\n",
                               session->proto,
                               &session->data->in_addr, ntohs(session->data->in_port),
                               &session->addr, ntohs(session->port),
                               session->data->timeout);
                } else {
                    seq_printf(m, "%d %pI4:%u -> %pI4:%u --- (will be removed due timeout)\n",
                               session->proto,
                               &session->data->in_addr, ntohs(session->data->in_port),
                               &session->addr, ntohs(session->port));
                }
                count++;
            }
        }
        rcu_read_unlock_bh();
    }

    seq_printf(m, "Total translations: %d\n", count);

    return 0;
}
static int nat_seq_open(struct inode *inode, struct file *file)
{
    return single_open(file, nat_seq_show, NULL);
}
static const struct file_operations nat_seq_fops = {
    .open		= nat_seq_open,
    .read		= seq_read,
    .llseek		= seq_lseek,
    .release	= single_release,
};


static int users_seq_show(struct seq_file *m, void *v)
{
    struct user_htable_ent *user;
    struct hlist_head *head;
    u_int32_t nataddr;
    unsigned int i, count;

    count=0;

    for (i = 0; i < users_hash_size; i++) {
        rcu_read_lock_bh();
        if (ht_users[i].use > 0) {
            head = &ht_users[i].user;
            hlist_for_each_entry_rcu(user, head, list_node) {
                if (user->idle < 15) {
                    nataddr = get_nat_addr(user->addr);
                    seq_printf(m, "%pI4 -> %pI4 (tcp: %u, udp: %u, other: %u)\n",
                               &user->addr,
                               &nataddr,
                               user->tcp_count,
                               user->udp_count,
                               user->other_count);
                    count++;
                }
            }
        }
        rcu_read_unlock_bh();
    }

    seq_printf(m, "Total users: %d\n", count);

    return 0;
}
static int users_seq_open(struct inode *inode, struct file *file)
{
    return single_open(file, users_seq_show, NULL);
}
static const struct file_operations users_seq_fops = {
    .open           = users_seq_open,
    .read           = seq_read,
    .llseek         = seq_lseek,
    .release        = single_release,
};

static int stat_seq_show(struct seq_file *m, void *v)
{
    seq_printf(m, "Active NAT sessions: %ld\n", atomic64_read(&sessions_active));
    seq_printf(m, "Tried NAT sessions: %ld\n", atomic64_read(&sessions_tried));
    seq_printf(m, "Created NAT sessions: %ld\n", atomic64_read(&sessions_created));
    seq_printf(m, "DNAT dropped pkts: %ld\n", atomic64_read(&dnat_dropped));
    seq_printf(m, "Fragmented pkts: %ld\n", atomic64_read(&frags));
    seq_printf(m, "Related ICMP pkts: %ld\n", atomic64_read(&related_icmp));
    seq_printf(m, "Active Users: %ld\n", atomic64_read(&users_active));

    return 0;
}
static int stat_seq_open(struct inode *inode, struct file *file)
{
    return single_open(file, stat_seq_show, NULL);
}
static const struct file_operations stat_seq_fops = {
    .open           = stat_seq_open,
    .read           = seq_read,
    .llseek         = seq_lseek,
    .release        = single_release,
};

#define SEPARATORS " ,;\t\n"
static int add_nf_destinations(const char *ptr)
{
    int len;

    for (; ptr; ptr += len) {
        struct sockaddr_storage ss;
        struct netflow_sock *usock;
        struct sockaddr_in *sin;
        const char *end;
        int succ = 0;

        /* skip initial separators */
        ptr += strspn(ptr, SEPARATORS);

        len = strcspn(ptr, SEPARATORS);
        if (!len)
            break;
        memset(&ss, 0, sizeof(ss));

        sin = (struct sockaddr_in *)&ss;

        sin->sin_family = AF_INET;
        sin->sin_port = htons(2055);
        succ = in4_pton(ptr, len, (u8 *)&sin->sin_addr, -1, &end);
        if (succ && *end == ':')
            sin->sin_port = htons(simple_strtoul(++end, NULL, 0));

        if (!succ) {
            printk(KERN_ERR "xt_NAT: can't parse netflow destination: %.*s\n",
                   len, ptr);
            continue;
        }

        if (!(usock = vmalloc(sizeof(*usock)))) {
            printk(KERN_ERR "xt_NAT: can't vmalloc socket\n");
            return -ENOMEM;
        }
        memset(usock, 0, sizeof(*usock));
        usock->addr = ss;
        list_add_tail(&usock->list, &usock_list);
        printk(KERN_INFO "xt_NAT NEL: add destination %s\n", print_sockaddr(&usock->addr));
    }
    return 0;
}

static struct xt_target nat_tg_reg __read_mostly = {
    .name     = "NAT",
    .revision = 0,
    .family   = NFPROTO_IPV4,
    .hooks    = (1 << NF_INET_FORWARD) | (1 << NF_INET_PRE_ROUTING) | (1 << NF_INET_POST_ROUTING),
    .target   = nat_tg,
    .targetsize = sizeof(struct xt_nat_tginfo),
    .me       = THIS_MODULE,
};

static int __init nat_tg_init(void)
{
    char buff[128] = { 0 };
    int i, j;

    printk(KERN_INFO "Module xt_NAT loaded\n");

    for(i=0, j=0; i<128 && nat_pool[i] != '-' && nat_pool[i] != '\0'; i++, j++) {
        buff[j] = nat_pool[i];
    }
    nat_pool_start = in_aton(buff);

    for(i++, j=0; i<128 && nat_pool[i] != '-' && nat_pool[i] != '\0'; i++, j++) {
        buff[j] = nat_pool[i];
    }
    nat_pool_end = in_aton(buff);

    if (nat_pool_start && nat_pool_end && nat_pool_start <= nat_pool_end ) {
        printk(KERN_INFO "xt_NAT DEBUG: IP Pool from %pI4 to %pI4\n", &nat_pool_start, &nat_pool_end);
        pool_table_create();
    } else {
        printk(KERN_INFO "xt_NAT DEBUG: BAD IP Pool from %pI4 to %pI4\n", &nat_pool_start, &nat_pool_end);
        return -1;
    }

    printk(KERN_INFO "xt_NAT DEBUG: NAT hash size: %d\n", nat_hash_size);
    printk(KERN_INFO "xt_NAT DEBUG: Users hash size: %d\n", users_hash_size);

    nat_htable_create();
    users_htable_create();
    pool_table_create();

    add_nf_destinations(nf_dest);

    proc_net_nat = proc_mkdir("NAT",init_net.proc_net);
    proc_create("sessions", 0644, proc_net_nat, &nat_seq_fops);
    proc_create("users", 0644, proc_net_nat, &users_seq_fops);
    proc_create("statistics", 0644, proc_net_nat, &stat_seq_fops);

    spin_lock_bh(&sessions_timer_lock);
    setup_timer( &sessions_cleanup_timer, sessions_cleanup_timer_callback, 0 );
    mod_timer( &sessions_cleanup_timer, jiffies + msecs_to_jiffies(10 * 1000) );
    spin_unlock_bh(&sessions_timer_lock);

    spin_lock_bh(&users_timer_lock);
    setup_timer( &users_cleanup_timer, users_cleanup_timer_callback, 0 );
    mod_timer( &users_cleanup_timer, jiffies + msecs_to_jiffies(60 * 1000) );
    spin_unlock_bh(&users_timer_lock);

    spin_lock_bh(&nfsend_lock);
    setup_timer( &nf_send_timer, nf_send_timer_callback, 0 );
    mod_timer( &nf_send_timer, jiffies + msecs_to_jiffies(1000) );
    spin_unlock_bh(&nfsend_lock);

    return xt_register_target(&nat_tg_reg);
}

static void __exit nat_tg_exit(void)
{
    xt_unregister_target(&nat_tg_reg);

    spin_lock_bh(&sessions_timer_lock);
    spin_lock_bh(&users_timer_lock);
    spin_lock_bh(&nfsend_lock);
    del_timer( &sessions_cleanup_timer );
    del_timer( &users_cleanup_timer );
    del_timer( &nf_send_timer );

    remove_proc_entry( "sessions", proc_net_nat );
    remove_proc_entry( "users", proc_net_nat );
    remove_proc_entry( "statistics", proc_net_nat );
    proc_remove(proc_net_nat);

    pool_table_remove();
    users_htable_remove();
    nat_htable_remove();

    while (!list_empty(&usock_list)) {
        struct netflow_sock *usock;

        usock = list_entry(usock_list.next, struct netflow_sock, list);
        list_del(&usock->list);
        if (usock->sock)
            sock_release(usock->sock);
        usock->sock = NULL;
        vfree(usock);
    }

    spin_unlock_bh(&sessions_timer_lock);
    spin_unlock_bh(&users_timer_lock);
    spin_unlock_bh(&nfsend_lock);

    printk(KERN_INFO "Module xt_NAT unloaded\n");
}

module_init(nat_tg_init);
module_exit(nat_tg_exit);

MODULE_DESCRIPTION("Xtables: Full Cone NAT");
MODULE_AUTHOR("Andrei Sharaev <andr.sharaev@gmail.com>");
MODULE_LICENSE("GPL");
MODULE_ALIAS("ipt_NAT");