/*
 *              IP_MASQ_ICQ icq masquerading module
 *
 *
 * Version:     @(#)ip_masq_icq.c 0.56	01 May 2000
 *
 * Author:      andrew deryabin <djsf@iname.com>
 * Homepage:	http://members.xoom.com/djsf/masq-icq/
 *
 * Fixes:
 *	Brandon Beretta	 : Proper loglevels for printk calls
 *	Alan Cox	 : GFP_ATOMIC priority for kmallocs that are in an interrupt
 *	Maurice Nonnekes : Fixed some warnings
 *	El Cabazorro	 : Fixed improper `rest' calculation in `v5crypt'
 *
 *	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.
 *
 *	ICQ protocol specs taken from icq-devel mailing list
 *	Thanx to Magnus Ihse and Sebastien Dault
 *
 *	To participate in the icq-devel mailing list, send a mail to
 *	majordomo@lists.realminfo.com, with the message body consisting
 *	only of the line "subscribe icq-devel".
 *
 *	To participate in the ip_masq_icq mailing list, send a mail to
 *	majordomo@access.ru, with the message body consisting
 *	only of the line "subscribe ip_masq_icq".
 *
 *	Best viewed with VIM	http://www.vim.org/
 *
 */

#define __KERNEL__
#define MODULE

#include <linux/config.h>
#ifdef CONFIG_MODVERSIONS
#include <linux/modversions.h>
#endif
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/ctype.h>
#include <linux/string.h>
#include <linux/skbuff.h>
#include <linux/in.h>
#include <linux/ip.h>
#include <net/protocol.h>
#include <net/udp.h>
#include <net/ip_masq.h>
#include <net/ip_masq_mod.h>

#define cl		((struct client *)ms->app_data)
#define tcpdir		((struct tcp_direct *)ms->app_data)

#define	icq_start_port	range[0]
#define	icq_end_port	range[1]

#define	LOG_LOGOUT	   1
#define	LOG_LOGIN	   2
#define	LOG_FWD		   4
#define LOG_FWD_OUT	   8
#define	LOG_CLOSE_FWD	  16
#define	LOG_FWD_SLAVE	  32
#define LOG_ONLINE	  64
#define LOG_OFFLINE	 128
#define LOG_INVIS	 256
#define LOG_LIMIT	 512
#define	LOG_ALL		1023

#ifndef GR_VERTICAL
#define GR_VERTICAL	'|'
#endif
#ifndef GR_LUCORNER
#define GR_LUCORNER	'/'
#endif

#ifndef GR_LBCORNER
#define GR_LBCORNER	'\\'
#endif

static int ports[MAX_MASQ_APP_PORTS] = { 4000 }, logmask = LOG_LOGIN;
static int range[] = { 60200, 61000 };
static int tcp_timeout = 14400, tcp_fin_timeout = 60, udp_timeout = 600,
	   icq_port, limit = 512, hide_local_ip = 1, hide_remote_local_ip = 0,
	   intranet = 1;
static char *log;

MODULE_DESCRIPTION ("ICQ Masquerading Module");
MODULE_AUTHOR ("djsf");
MODULE_PARM (ports, "1-" __MODULE_STRING(MAX_MASQ_APP_PORTS) "i");
MODULE_PARM (tcp_timeout, "i");
MODULE_PARM (tcp_fin_timeout, "i");
MODULE_PARM (udp_timeout, "i");
MODULE_PARM (range, "2i");
MODULE_PARM (log, "0-1s");
MODULE_PARM (limit, "i");
MODULE_PARM (hide_local_ip, "i");
MODULE_PARM (hide_remote_local_ip, "i");
MODULE_PARM (intranet, "i");

static struct ip_masq_app *masq_incarnations[MAX_MASQ_APP_PORTS];

#ifndef __NO_KSPC_CLISTS__
struct online {
    struct online *next;
    __u32	   uin, addr;
    __u16	   port;
};
#endif

static struct client {
    struct client *next;
#ifndef __NO_KSPC_CLISTS__
    struct online *online;
#endif
    __u32	   uin, cl_addr;
    __u16	   cl_port, masq_port;
#ifndef __NO_KSPC_CLISTS__
    __u16	   online_n;
#endif
} *cl_base = NULL;

struct tcp_direct {
    __u32 uin, last_seq;
    __u16 pktlen, pktlen_new;
    __u8  pktstart, pktstart_new;
};

/*****************************************************************************/

static struct client *cl_lookup (__u16 port)
{
    struct client *p = cl_base;
    for (; p; p = p->next)
	if (p->masq_port == port)
	    return p;
    return NULL;
}

#ifdef __LITTLE_ENDIAN

static inline __u16 get_le16 (__u8 *p)
{
    return *(__u16 *)p;
}

static inline __u32 get_le32 (__u8 *p)
{
    return *(__u32 *)p;
}

static inline void put_le16 (__u8 *p, __u16 d)
{
    *(__u16 *)p = d;
}

static inline void put_le32 (__u8 *p, __u32 d)
{
    *(__u32 *)p = d;
}

#else	/* any other bytesex */

static inline __u16 get_le16 (__u8 *p)
{
    return (p[1] << 8) + *p;
}

static inline __u32 get_le32 (__u8 *p)
{
    return (p[3] << 24) | (p[2] << 16) | (p[1] << 8) | *p;
}

static inline void put_le16 (__u8 *p, __u16 d)
{
    *p++ = d;
    *p = d >> 8;
}

static inline void put_le32 (__u8 *p, __u32 d)
{
    *p++ = d;
    *p++ = d >> 8;
    *p++ = d >> 16;
    *p = d >> 24;
}
#endif


static inline int alloc_mport (void)
{
    int started_from = icq_port;

    for (; icq_port < icq_end_port; icq_port++)
	if (!cl_lookup (htons (icq_port)))
	    return htons (icq_port++);	/* TODO lookup port in masq table too */
    for (icq_port = icq_start_port; icq_port < started_from; icq_port++)
	if (!cl_lookup (htons (icq_port)))
	    return htons (icq_port++);

    return 0;
}

static inline void v4_put_checkcode (__u8 *data)
{
    __u8 *p = data + (*data == 4 ? 16 : 12);

    *p++ = data[6] ^ 0xF5;    /* NUMBER2 == 0x01FF00F5 is valid for all packets of version < 256 :) */
    *p++ = data[2];
    *p++ = ~data[4];
    *p = data[8] ^ 1;
}

static inline __u32 v5_calc_checkcode (__u8 *data)
{
    return ((data[8] ^ 0x1F) << 24) | (0xFF0000&((data[4] ^ ~data[0x1F]) << 16))
	  | (data[2] << 8) | (data[6] ^ 0xA6);
}

static inline __u32 v4code2 (__u8 *data, unsigned len)
{
    return len*0x66756B65 + get_le32 (data+16);
}

static inline __u32 v5code2 (unsigned len, __u32 c)
{
    return len*0x68656C6C + c;
}

static __u8 v4table[] = {
    0x0A, 0x20, 0x20, 0x20, 0x69, 0x74, 0x73, 0x64,
    0x43, 0x61, 0x2E, 0x73, 0x65, 0x74, 0x6F, 0x73,
    0x72, 0x74, 0x22, 0x66, 0x6E, 0x2F, 0x63, 0x6E,
    0x51, 0x20, 0x6D, 0x65, 0x6F, 0x73, 0x6E, 0x65,
    0x6E, 0x6C, 0x6E, 0x20, 0x64, 0x20, 0x6E, 0x5D,
    0x6E, 0x72, 0x6D, 0x20, 0x74, 0x73, 0x69, 0x20,
    0x62, 0x63, 0x6B, 0x20, 0x73, 0x74, 0x65, 0x20,
    0x6C, 0x6F, 0x6C, 0x65, 0x67, 0x73, 0x20, 0x6F
};

static __u8 v5table[] = {
    0x4C, 0x5B, 0x6D, 0x6F, 0x4C, 0x63, 0x5F, 0x43,
    0x31, 0x4A, 0x67, 0x6C, 0x4D, 0x69, 0x44, 0x48,
    0x6C, 0x51, 0x6C, 0x55, 0x48, 0x6D, 0x5F, 0x47,
    0x67, 0x35, 0x6E, 0x6D, 0x5F, 0x4B, 0x62, 0x4D,
    0x5D, 0x58, 0x58, 0x48, 0x59, 0x31, 0x32, 0x52,
    0x64, 0x4E, 0x53, 0x30, 0x59, 0x3D, 0x3A, 0x52,
    0x48, 0x49, 0x46, 0x50, 0x6F, 0x25, 0x54, 0x5D,
    0x50, 0x3F, 0x69, 0x54, 0x31, 0x00, 0x37, 0x46
};

static void v4crypt (__u8 *data, unsigned len, __u32 c2)
{
    int i;
    __u8 *p;

    for (p = data, i = 0, len = (len+3)/16; i < len; p += 4, i++)
	put_le32 (p, get_le32(p) ^ (c2 + v4table[i & 0x3F]));
    *data++ = 4; *data = 0;
}

static void v5crypt (__u8 *data, unsigned len, __u32 c2)
{
    int i, rest = (len-2)&3;
    __u8 *p;

    for (p = data+10, i = 0, len = (len-10)/4; i < len; p += 4, i++)
	put_le32 (p, get_le32(p) ^ (c2 + v5table[i & 0x3F]));
    if (rest) {
	c2 += v5table[i & 0x3F];
	do {
	    *p++ ^= c2;
	    c2 >>= 8;
	} while (--rest);
    }
}

static inline __u32 v5_extract_checkcode (__u8 *data)
{
    __u32 c = get_le32 (data+20);

    return ((c & 0x0001F000) >> 12) + ((c & 0x07C007C0) >> 1)
	 + ((c & 0x003E0001) << 10) + ((c & 0xF8000000) >> 16)
	 + ((c & 0x0000083E) << 15);
}

static inline void v5_insert_checkcode (__u8 *data, __u32 c)
{
    put_le32 (data+20, ((c & 0x0000001F) << 12) + ((c & 0x03E003E0) << 1)
		     + ((c & 0xF8000400) >> 10) + ((c & 0x0000F800) << 16)
		     + ((c & 0x041F0000) >> 15));
}

static void update_tcp_range (void)
{
    if (icq_start_port > icq_end_port) {
	int tmp = icq_start_port;
	icq_start_port = icq_end_port; icq_end_port = tmp;
    }
    printk (KERN_INFO "ip_masq_icq: using TCP port range %u-%u\n", icq_port = icq_start_port, icq_end_port);
}

/*****************************************************************************/

static struct ip_masq_mod icq_mod;
static struct ip_masq_timeout_table icq_timeout_table;

static int masq_icq_start (struct ip_masq_app *mapp, struct ip_masq *ms)
{
    ip_masq_timeout_attach (ms, &icq_timeout_table);
    if (!(ms->app_data = kmalloc (sizeof (struct client), GFP_ATOMIC)))
	return -ENOMEM;
    cl->next = cl_base; cl_base = cl;
    cl->masq_port = 0;
#ifndef __NO_KSPC_CLISTS__
    cl->online = NULL;
    cl->online_n = 0;
#endif
    ip_masq_mod_inc_nent (&icq_mod);
    MOD_INC_USE_COUNT;
    return 0;
}

static int masq_icq_fin (struct ip_masq_app *mapp, struct ip_masq *ms)
{
    ip_masq_mod_dec_nent (&icq_mod);
    ip_masq_timeout_detach (ms);
    if (cl) {
	struct client **p = &cl_base;
#ifndef __NO_KSPC_CLISTS__
	struct online *ponl = cl->online, *ponl_next;

	for (; ponl; ponl = ponl_next) {
	    ponl_next = ponl->next;
	    kfree (ponl);
	}
#endif
	if (cl->masq_port)
	    if (logmask & LOG_LOGOUT)
		printk (KERN_INFO "ip_masq_icq: LOGOUT %u@%u.%u.%u.%u:%u<-%u/%u\n",
			cl->uin, NIPQUAD (cl->cl_addr), ntohs (cl->cl_port), ntohs (cl->masq_port), ntohs (ms->sport));
	for (; *p != cl; p = &(*p)->next)
	    if (!*p) {
		printk (KERN_ERR "ip_masq_icq: broken client list, prepare for kernel panic\n");
		return -EFAULT;
	    }
	*p = cl->next;
	kfree (cl); cl = NULL;
    }
    MOD_DEC_USE_COUNT;
    return 0;
}

static int masq_icq_pkt_out (struct ip_masq_app *mapp, struct ip_masq *ms,
			     struct sk_buff **skb_p, __u32 maddr)
{
    struct sk_buff *skb = *skb_p;
    struct iphdr *iph = skb->nh.iph;
    __u8 *data = (__u8 *)((struct udphdr *)&(((__u8 *)iph)[iph->ihl << 2])+1), *port_ptr;
    __u16 from_port;
    __u32 c2;
    unsigned len = skb->len - (data - skb->h.raw);

    if (data[1] || len < 32 || !cl)
	return 0;
    switch (*data) {
	case 2:	    if (get_le16 (data+2) != 0x3E8) return 0;
		    port_ptr = data + 10; break;
	case 3:	    if (get_le16 (data+2) != 0x3E8) return 0;
		    port_ptr = data + 20; break;
	case 4:	    c2 = v4code2 (data, len);
		    if ((get_le16 (data+6) ^ (c2 >> 16)) != 0x3E8) return 0;
		    port_ptr = data + 24; v4crypt (data, len, c2); break;
	case 5:	    c2 = v5code2 (len, v5_extract_checkcode (data));
		    if ((get_le16 (data+14) ^ (0xFFFF&(c2+0x5B))) != 0x3E8) return 0;
		    port_ptr = data + 28; v5crypt (data, len, c2); break;
	default:    return 0;
    }
    from_port = get_le16 (port_ptr);

    if (cl->masq_port) {
	if (!from_port) {
	    cl->masq_port = 0; goto get_out;
	}
    } else {
	if (!from_port)
	    goto get_out;
	cl->masq_port = alloc_mport();
    }
    cl->cl_addr = ms->saddr; cl->cl_port = htons (from_port);

    port_ptr[0] = ((__u8 *)&cl->masq_port)[1];	/* ICQ order is LE, net order is BE */
    port_ptr[1] = ((__u8 *)&cl->masq_port)[0];

get_out:
    if (hide_local_ip && len > get_le16 (data+14)+24)
	*(__u32 *)(data+get_le16 (data+14)+20) = maddr;
    switch (*data) {
	case 2:	 cl->uin = get_le32 (data+6); break;
	case 3:  cl->uin = get_le32 (data+8);
		 v4_put_checkcode (data); break;
	case 4:  cl->uin = get_le32 (data+12);
		 v4_put_checkcode (data);
		 v4crypt (data, len, v4code2 (data, len)); break;
	case 5:	 cl->uin = get_le32 (data+6);
		 v5crypt (data, len, v5code2 (len, c2 = v5_calc_checkcode (data)));
		 v5_insert_checkcode (data, c2); break;
    }

    if (logmask & LOG_LOGIN)
	printk (KERN_INFO "ip_masq_icq: LOGIN %u@%u.%u.%u.%u:%u<-%u/%u, protocol v%u\n",
		cl->uin, NIPQUAD (cl->cl_addr), from_port, ntohs (cl->masq_port), ntohs (ms->sport), *data);
    return 0;
}

#ifndef __NO_KSPC_CLISTS__
static void add_online (struct client *pcl, __u8 *param_p, __u32 maddr, int *altered)
{
    struct online *ponl;
    struct client *lcl;
    char rmt_loc[20];
    __u32 uin = get_le32 (param_p);

    if (logmask & LOG_ONLINE) {
	if (*(__u32 *)(param_p+4) == *(__u32 *)(param_p+12))
	    *rmt_loc = 0;
	else
	    sprintf (rmt_loc, " (%u.%u.%u.%u)", NIPQUAD (param_p[12]));
	printk (KERN_INFO "ip_masq_icq: USER_ONLINE %u@%u.%u.%u.%u%s:%u appears in %u@%u.%u.%u.%u:%u<-%u's contact list\n",
		uin, NIPQUAD (param_p[4]), rmt_loc, get_le16 (param_p+8), pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (pcl->masq_port));
    }
    if (intranet && *(__u32 *)(param_p+4) == maddr && (lcl = cl_lookup (htons (get_le16 (param_p+8))))) {
	*(__u32 *)(param_p+4) = lcl->cl_addr;
	put_le16 (param_p+8, ntohs (lcl->cl_port));
	*altered = 1;
    }
    if (hide_remote_local_ip) {
	*(__u32 *)(param_p+12) = *(__u32 *)(param_p+4);
	*altered = 1;
    }
    for (ponl = pcl->online; ponl; ponl = ponl->next)
	if (ponl->uin == uin) goto found_online;
    if (++pcl->online_n > limit) {
	pcl->online_n = limit;
	if (logmask & LOG_LIMIT)
	    printk (KERN_WARNING "ip_masq_icq: online list size limit for %u reached\n", pcl->uin);
	return;
    }
    if (!(ponl = (struct online *)kmalloc (sizeof (struct online), GFP_ATOMIC))) {
	pcl->online_n--;
	return;
    }
    ponl->next = pcl->online; pcl->online = ponl;

found_online:
    ponl->uin = uin;
    ponl->addr = *(__u32 *)(param_p+4);
    ponl->port = htons (get_le16 (param_p+8));
}

static void del_online (struct client *pcl, __u8 *param_p)
{
    struct online *ponl, **pponl;
    __u32 uin = get_le32 (param_p);

    for (pponl = &pcl->online; *pponl; pponl = &(*pponl)->next)
	if ((*pponl)->uin == uin) {
	    if (logmask & LOG_OFFLINE)
		printk (KERN_INFO "ip_masq_icq: USER_OFFLINE %u@%u.%u.%u.%u:%u disappears from %u@%u.%u.%u.%u:%u<-%u's contact list\n",
			uin, NIPQUAD ((*pponl)->addr), ntohs ((*pponl)->port), pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (pcl->masq_port));
	    *pponl = (ponl = *pponl)->next;
	    kfree (ponl);
	    pcl->online_n--;
	    return;
	}
    if (logmask & LOG_INVIS)
	printk (KERN_INFO "ip_masq_icq: %u is possibly invisible for %u@%u.%u.%u.%u:%u<-%u\n",
		uin, pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (pcl->masq_port));
}

static int masq_icq_pkt_in (struct ip_masq_app *mapp, struct ip_masq *ms,
			    struct sk_buff **skb_p, __u32 maddr)
{
    struct sk_buff *skb = *skb_p;
    struct iphdr *iph = skb->nh.iph;
    int altered = 0;
    __u8 *data = (__u8 *)((struct udphdr *)&(((__u8 *)iph)[iph->ihl << 2])+1), *p, *lim;
    unsigned len = skb->len - (data - skb->h.raw), i, ofs;

    if (data[1] || len < 10 || !cl)
	return 0;
    switch (*data) {
	case 2:	    switch (get_le16 (data+2)) {
			case 0x6E:  if (len > 30)
					add_online (cl, data+6, maddr, &altered);
				    return 0;
			case 0x78:  del_online (cl, data+6);
		    }
	case 3:	    p = data+2; ofs = 4; break;
	case 5:	    p = data+7; ofs = 9; break;
	default:    return 0;
    }
    switch (get_le16 (p)) {
	case 0x212: if (len > 28 && (i = (p+=15)[-1]))
			for (lim = data+len-18;;) {
			    switch (get_le16 (p+ofs)) {
				case 0x6E:  if (p+ofs+11 >= lim) goto check_altered;
					    add_online (cl, p+ofs+14, maddr, &altered);
					    break;
				case 0x78:  if (p+ofs >= lim) goto check_altered;
					    del_online (cl, p+ofs+14);
			    }
			    if (!--i || (p += 2 + get_le16 (p)) >= lim) break;
			}
		    break;
	case 0x6E:  if (len > 40)
			add_online (cl, p+14, maddr, &altered);
		    break;
	case 0x78:  if (len > ofs+15)
    			del_online (cl, p+14);
    }
check_altered:
    if (altered && *data == 3)
	v4_put_checkcode (data);
    return 0;
}
#else /* __NO_KSPC_CLISTS__ */
#define masq_icq_pkt_in NULL
#endif

static struct ip_masq_app ip_masq_icq = {
    NULL,			/* next		   */
    "icq",			/* name		   */
    0,				/* type		   */
    0,				/* n_attach	   */
    masq_icq_start,		/* ip_masq_init_1  */
    masq_icq_fin,		/* ip_masq_done_1  */
    masq_icq_pkt_out,		/* pkt_out	   */
    masq_icq_pkt_in		/* pkt_in	   */
};

/*****************************************************************************/

static struct ip_masq_timeout_table icq_timeout_table = {
    ATOMIC_INIT(0), /* refcnt */
    0,		    /* scale  */
    {
	    30*60*HZ,	    /*	    IP_MASQ_S_NONE,	    */
	    15*60*HZ,	    /*	    IP_MASQ_S_ESTABLISHED,  */
	    2*60*HZ,	    /*	    IP_MASQ_S_SYN_SENT,	    */
	    1*60*HZ,	    /*	    IP_MASQ_S_SYN_RECV,	    */
	    2*60*HZ,	    /*	    IP_MASQ_S_FIN_WAIT,	    */
	    2*60*HZ,	    /*	    IP_MASQ_S_TIME_WAIT,    */
	    10*HZ,	    /*	    IP_MASQ_S_CLOSE,	    */
	    60*HZ,	    /*	    IP_MASQ_S_CLOSE_WAIT,   */
	    30*HZ,	    /*	    IP_MASQ_S_LAST_ACK,	    */
	    2*60*HZ,	    /*	    IP_MASQ_S_LISTEN,	    */
	    5*60*HZ,	    /*	    IP_MASQ_S_UDP,	    */
	    1*60*HZ,	    /*	    IP_MASQ_S_ICMP,	    */
	    2*HZ,	    /*	    IP_MASQ_S_LAST	    */
    },      /* timeout */
};  /* copied from ip_masq.c */

static void update_timeouts (void)
{
    icq_timeout_table.timeout[IP_MASQ_S_ESTABLISHED] = HZ*tcp_timeout;
    icq_timeout_table.timeout[IP_MASQ_S_FIN_WAIT] =
    icq_timeout_table.timeout[IP_MASQ_S_TIME_WAIT] = HZ*tcp_fin_timeout;
    icq_timeout_table.timeout[IP_MASQ_S_UDP] = HZ*udp_timeout;
}

static struct ip_masq_app ip_masq_icq_direct;

static int icq_in_rule (const struct sk_buff *skb, const struct iphdr *iph)
{
    if (iph->protocol != IPPROTO_TCP)
	return IP_MASQ_MOD_NOP;
    return cl_lookup (((__u16 *)&(((__u8 *)iph)[iph->ihl << 2]))[1]) ? IP_MASQ_MOD_ACCEPT : IP_MASQ_MOD_NOP;
}

static struct ip_masq *icq_in_create (const struct sk_buff *skb, const struct iphdr *iph, __u32 maddr)
{
    __u16 *portp = (__u16 *)&(((__u8 *)iph)[iph->ihl << 2]);
    struct client *pcl;
    struct ip_masq *ms;
    struct tcp_direct *mem;

    if (iph->protocol != IPPROTO_TCP || iph->daddr != maddr || !(pcl = cl_lookup (portp[1])))
	return NULL;

    if (logmask & LOG_FWD)
	printk (KERN_INFO "ip_masq_icq: forwarding connection %u.%u.%u.%u:%u -> %u@%u.%u.%u.%u:%u<-%u\n",
		NIPQUAD (iph->saddr), ntohs (*portp), pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (portp[1]));
    if (!(mem = kmalloc (sizeof (struct tcp_direct), GFP_ATOMIC)))
	return NULL;
    if (!(ms = ip_masq_new (IPPROTO_TCP, maddr, portp[1], pcl->cl_addr, pcl->cl_port, iph->saddr, *portp, 0))) {
	printk (KERN_WARNING "ip_masq_icq: cannot create masq table entry for incoming connection\n");
	kfree (mem); return NULL;
    }
    ip_masq_timeout_attach (ms, &icq_timeout_table);
    ms->app = &ip_masq_icq_direct; ms->app_data = mem;
    ip_masq_icq_direct.n_attach++;
    mem->uin = pcl->uin;
    memset (&(mem->last_seq), 0, sizeof (struct tcp_direct) - ((__u8 *)&(mem->last_seq) - (__u8 *)mem));
    MOD_INC_USE_COUNT;
    ip_masq_listen (ms);
    return ms;
}

#ifndef __NO_KSPC_CLISTS__
static struct ip_masq *icq_out_create (const struct sk_buff *skb, const struct iphdr *iph, __u32 maddr)
{
    __u16 *portp = (__u16 *)&(((__u8 *)iph)[iph->ihl << 2]);
    struct client *pcl;
    struct ip_masq *ms;
    struct online *ponl;
    struct tcp_direct *mem;

    if (iph->protocol == IPPROTO_TCP)
	for (pcl = cl_base; pcl; pcl = pcl->next)
	    if (iph->saddr == pcl->cl_addr)
		for (ponl = pcl->online; ponl; ponl = ponl->next)
		    if (iph->daddr == ponl->addr && portp[1] == ponl->port) {
			if (logmask & LOG_FWD_OUT)
			    printk (KERN_INFO "ip_masq_icq: forwarding connection %u@%u.%u.%u.%u:%u <- %u@%u.%u.%u.%u:%u\n",
				    ponl->uin, NIPQUAD (iph->daddr), ntohs (portp[1]), pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (*portp));
			if (!(mem = kmalloc (sizeof (struct tcp_direct), GFP_ATOMIC)))
			    return NULL;
			if (!(ms = ip_masq_new (IPPROTO_TCP, maddr, 0, iph->saddr, *portp, iph->daddr, portp[1], 0))) {
			    printk (KERN_WARNING "ip_masq_icq: cannot create masq table entry for outgoing connection\n");
			    kfree (mem); return NULL;
			}
			ip_masq_timeout_attach (ms, &icq_timeout_table);
			ms->app = &ip_masq_icq_direct; ms->app_data = mem;
			ip_masq_icq_direct.n_attach++;
			mem->uin = pcl->uin;
			memset (&(mem->last_seq), 0, sizeof (struct tcp_direct) - ((__u8 *)&(mem->last_seq) - (__u8 *)mem));
			MOD_INC_USE_COUNT;
			return ms;
		    }
    return NULL;
}
#else /* __NO_KSPC_CLISTS__ */
#define icq_out_create NULL
#endif

#ifdef CONFIG_PROC_FS
static struct proc_dir_entry icq_proc_root;
#define icq_proc_root_p &icq_proc_root
#else /* ! CONFIG_PROC_FS */
#define icq_proc_root_p NULL
#endif

static struct ip_masq_mod icq_mod = {
    NULL,			/* next		   */
    NULL,			/* next_reg	   */
    "icq",			/* mmod_name	   */
    ATOMIC_INIT(0),		/* refcnt	   */
    ATOMIC_INIT(0),		/* mmod_nent	   */
    icq_proc_root_p,		/* mmod_proc_ent   */
    NULL,			/* mmod_ctl	   */
    NULL,			/* mmod_init	   */
    NULL,			/* mmod_done	   */
    icq_in_rule,		/* mmod_in_rule	   */
    NULL,			/* mmod_in_update  */
    icq_in_create,		/* mmod_in_create  */
    NULL,			/* mmod_out_rule   */
    NULL,			/* mmod_out_update */
    icq_out_create		/* mmod_out_create */
};

/*****************************************************************************/

static int icq_direct_fin (struct ip_masq_app *mapp, struct ip_masq *ms)
{
    if (logmask & LOG_CLOSE_FWD) {
	int mp = ntohs (ms->mport);
	printk (KERN_INFO "ip_masq_icq: closing connection %u.%u.%u.%u:%u %s %u@%u.%u.%u.%u:%u<-%u\n",
    		NIPQUAD (ms->daddr), ntohs (ms->dport), PORT_MASQ_BEGIN <= mp && mp <= PORT_MASQ_END ? "<-" : "->",
		tcpdir->uin, NIPQUAD (ms->saddr), ntohs (ms->sport), mp);
    }
    if (ms->app_data) {
	kfree (ms->app_data); ms->app_data = NULL;
    }
    ip_masq_timeout_detach (ms);
    MOD_DEC_USE_COUNT;
    return 0;
}

static int icq_direct_out (struct ip_masq_app *mapp, struct ip_masq *ms, struct sk_buff **skb_p, __u32 maddr)
{
    struct ip_masq *fcms;
    struct sk_buff *skb = *skb_p;
    struct iphdr *iph = skb->nh.iph;
    struct tcphdr *th = (struct tcphdr *)&(((__u8 *)iph)[iph->ihl << 2]);
    __u8 *data = (__u8 *)th + (th->doff << 2), *p;
    __u16 fcport, pktlen;
    unsigned len = skb->len - (data - skb->h.raw), st;

    if (th->syn || th->fin || th->rst)
	return 0;

    if (tcpdir->last_seq != th->seq) {
	tcpdir->last_seq = th->seq;
	pktlen = tcpdir->pktlen = tcpdir->pktlen_new;
	tcpdir->pktstart = tcpdir->pktstart_new;
    } else
	pktlen = tcpdir->pktlen;

    if (tcpdir->pktstart) {
	tcpdir->pktstart_new = 0; goto pktstart;
    }

    while (pktlen < len) {
	len -= pktlen;
	if (len < 2) {				/* TODO */
	    printk (KERN_WARNING "ip_masq_icq: %u@%u.%u.%u.%u:%u's client is bad and mad and not supported yet\n", tcpdir->uin, NIPQUAD (ms->saddr), ntohs (ms->sport));
	    pktlen = 65535; goto get_out;
	}
	pktlen = get_le16 (data += pktlen);
	if (!(len -= 2)) {
	    tcpdir->pktstart_new = 1; goto get_out;
	}
	data += 2;
pktstart:
	if (len > 41) {
	    if (get_le16 (data+6) == 0x7DA)			/* v3 */
		p = data+14;
	    else if (((__u16 *)data)[1] == ((__u16 *)data)[8])	/* ICQ98 */
		p = data+18;
	    else if ((st = get_le16 (data+22)) == 2		/* ICQ99 chat */
		      && *(__u16 *)(data+34)
		      && ntohs (*(__u16 *)(data+34)) == get_le16 (data+38)) {
		p = data+9; fcport = *(__u16 *)(data+34);
		if (logmask & LOG_FWD_SLAVE)
		    printk (KERN_INFO "ip_masq_icq: chat request from %u.%u.%u.%u:%u accepted by %u@%u.%u.%u.%u:%u using ICQ99 protocol\n",
			    NIPQUAD (ms->daddr), ntohs (ms->dport), tcpdir->uin, NIPQUAD (ms->saddr), ntohs (fcport));
		goto create_secondary;
	    } else if (st == 3					/* ICQ99 file xfer */
		       && *(__u16 *)(data+31) && get_le16 (data+35) + 42 < len
		       && ntohs (*(__u16 *)(data+31)) == get_le16 (data+get_le16 (data+35) + 41)) {
		p = data+9; fcport = *(__u16 *)(data+31);
		if (logmask & LOG_FWD_SLAVE)
		    printk (KERN_INFO "ip_masq_icq: file xfer request from %u.%u.%u.%u:%u accepted by %u@%u.%u.%u.%u:%u using ICQ99 protocol\n",
			    NIPQUAD (ms->daddr), ntohs (ms->dport), tcpdir->uin, NIPQUAD (ms->saddr), ntohs (fcport));
		goto create_secondary;
	    } else
		continue;
	    if (get_le16 (p+2) != 1 || len < 49) /* TODO support msglengths other than 1 */
		continue;
	    switch (st = get_le16 (p)) {
		case 2:	    if (!(fcport = *(__u16 *)(p+25)))
				continue;
			    if (logmask & LOG_FWD_SLAVE)
				printk (KERN_INFO "ip_masq_icq: chat request from %u.%u.%u.%u:%u accepted by %u@%u.%u.%u.%u:%u using protocol v%u\n",
					NIPQUAD (ms->daddr), ntohs (ms->dport), tcpdir->uin, NIPQUAD (ms->saddr), ntohs (fcport), get_le16 (data+4));
			    break;
		case 3:	    if (!(fcport = *(__u16 *)(p+22)))
				continue;
			    if (logmask & LOG_FWD_SLAVE)
				printk (KERN_INFO "ip_masq_icq: file xfer request from %u.%u.%u.%u:%u accepted by %u@%u.%u.%u.%u:%u using protocol v%u\n",
					NIPQUAD (ms->daddr), ntohs (ms->dport), tcpdir->uin, NIPQUAD (ms->saddr), ntohs (fcport), get_le16 (data+4));
			    break;
		default:    continue;
	    }
create_secondary:
	    if (!(fcms = ip_masq_new (IPPROTO_TCP, maddr, 0, ms->saddr, fcport, ms->daddr, 0, IP_MASQ_F_NO_DPORT))) {
		printk (KERN_WARNING "ip_masq_icq: cannot create masq table entry for slave connection\n");
		continue;
	    }
	    if (st == 2) {				/* chat	 */
		*(__u16 *)(p+25) = fcms->mport;
		put_le16 (p+29, ntohs (fcms->mport));
		ip_masq_timeout_attach (fcms, &icq_timeout_table); /* TODO detach it somewhere */
	    } else {					/* file xfer */
		*(__u16 *)(p+22) = fcms->mport;
		if ((st = get_le16 (p+26) + 32) < len-1) {
		    put_le16 (p + st, ntohs (fcms->mport));
		}
	    }
	    ip_masq_control_add (fcms, ms);
	    ip_masq_listen (fcms); ip_masq_put (fcms);
	}
    }
    pktlen -= len;
get_out:
    tcpdir->pktlen_new = pktlen;
    return 0;
}

static struct ip_masq_app ip_masq_icq_direct = {
    NULL,			/* next		   */
    "icq_direct",		/* name		   */
    0,				/* type		   */
    0,				/* n_attach	   */
    NULL,			/* ip_masq_init_1  */
    icq_direct_fin,		/* ip_masq_done_1  */
    icq_direct_out,		/* pkt_out	   */
    NULL			/* pkt_in	   */
};

/*****************************************************************************/

#ifdef CONFIG_PROC_FS
static int read_onoff (char *buffer, char **start, off_t offset,
			int count, int *eof, void *data)
{
    *buffer = 'o';
    if (*(int *)data) {
	buffer[1] = 'n'; buffer[2] = '\n'; return 3;
    }
    buffer[1] = buffer[2] = 'f'; buffer[3] = '\n'; return 4;
}

static int write_onoff (struct file *file, const char *buffer,
			unsigned long count, void *data)
{
    if (count == 1 || (count == 2 && buffer[1] == '\n'))
	switch (*buffer) {
clr_data:   case '0':   *(int *)data = 0; return count;
set_data:   case '1':   *(int *)data = 1; return count;
	    default:    return -EINVAL;
	}
    if (*buffer == 'o') {
	if (buffer[1] == 'n' && (count == 2 || (count == 3 && buffer[2] == '\n')))
	    goto set_data;
	if (buffer[1] == 'f' && buffer[2] == 'f' && (count == 3 || (count == 4 && buffer[3] == '\n')))
	    goto clr_data;
    }
    return -EINVAL;
}

static int read_int (char *buffer, char **start, off_t offset,
			int count, int *eof, void *data)
{
    return sprintf (buffer, "%u\n", *(int *)data);
}

static int write_int (struct file *file, const char *buffer,
			unsigned long count, void *data)
{
    char *p, buff[16];
    unsigned long l;

    if (!isdigit (*buffer))
	return -EINVAL;
    l = simple_strtoul (strncpy (buff, buffer, 16), &p, 0);
    if (*p && *p != '\t' && *p != '\n' && *p != ' ')
	return -EINVAL;
    *(int *)data = l;
    update_timeouts();
    return count;
}

static int read_range (char *buffer, char **start, off_t offset,
			int count, int *eof, void *data)
{
    return sprintf (buffer, "%u-%u\n", *(int *)data, ((int *)data)[1]);
}

static int write_range (struct file *file, const char *buffer,
			unsigned long count, void *data)
{
    char *p, buff[16];
    unsigned long first, second;

    if (!isdigit (*buffer))
	return -EINVAL;
    first = simple_strtoul (strncpy (buff, buffer, 16), &p, 0);
    while (*p == '\t' || *p == ' ' || *p == ',' || *p == '-')
	p++;
    if (!isdigit (*p))
	return -EINVAL;
    second = simple_strtoul (p, &p, 0);
    if (*p && *p != '\t' && *p != '\n' && *p != ' ')
	return -EINVAL;
    *(int *)data = first;
    ((int *)data)[1] = second;
    update_tcp_range();
    return count;
}

static inline char check_log_opt (int mask)
{
    return logmask & mask ? '+' : '-';
}

static int read_log (char *buffer, char **start, off_t offset,
			int count, int *eof, void *data)
{
    return sprintf (buffer, "%co Client Logout\n"
			    "%ci Client Login\n"
			    "%cf Forwarding incoming direct connection\n"
			    "%cd Forwarding outgoing direct connection\n"
			    "%cc Closing the forwarded connection\n"
			    "%cs Forwarding secondary connection\n"
			    "%cl User on client's contact list is online\n"
			    "%cn User on client's contact list is offline\n"
			    "%cx User is _possibly_ invisible for client\n"
			    "%cm Online users list for specified client is overflowed\n%s",
			    check_log_opt (LOG_LOGOUT), check_log_opt (LOG_LOGIN),
			    check_log_opt (LOG_FWD), check_log_opt (LOG_FWD_OUT),
			    check_log_opt (LOG_CLOSE_FWD), check_log_opt (LOG_FWD_SLAVE),
			    check_log_opt (LOG_ONLINE), check_log_opt (LOG_OFFLINE),
			    check_log_opt (LOG_INVIS), check_log_opt (LOG_LIMIT),
			    logmask == LOG_ALL ? "+a All logging options are turned on\n" : "");
}
#endif	/* CONFIG_PROC_FS */

static int write_log (struct file *file, const char *optstr,
			unsigned long count, void *data)
{
    int lm = logmask, opmask=0, len;
    enum {
	PUT=0, CLR, SET
    } op = PUT;

    void apply_opt (void)
    {
	switch (op) {
	    case PUT:	if (opmask || (len & (1 << (8*sizeof (len) - 1)) && !*optstr))
			    lm = opmask; break;
	    case CLR:	lm &= ~opmask; break;
	    case SET:	lm |= opmask; break;
	}
	opmask = 0;
    }

    for (len = count; len && *optstr; len--, optstr++)
	switch (*optstr) {
	    case 'o':   opmask |= LOG_LOGOUT;	 break;
	    case 'i':   opmask |= LOG_LOGIN;	 break;
	    case 'f':   opmask |= LOG_FWD;	 break;
	    case 'd':   opmask |= LOG_FWD_OUT;	 break;
	    case 'c':   opmask |= LOG_CLOSE_FWD; break;
	    case 's':   opmask |= LOG_FWD_SLAVE; break;
	    case 'l':   opmask |= LOG_ONLINE;	 break;
	    case 'n':   opmask |= LOG_OFFLINE;	 break;
	    case 'x':   opmask |= LOG_INVIS;	 break;
	    case 'm':   opmask |= LOG_LIMIT;	 break;
	    case 'a':   opmask |= LOG_ALL;
	    case '\t':
	    case '\n':
	    case ' ':	break;
	    case '+':	apply_opt(); op = SET; break;
	    case '-':	apply_opt(); op = CLR; break;
	    default:    printk (KERN_WARNING "ip_masq_icq: unknown log option `%c'\n", *optstr);
			return -EINVAL;
	}
    apply_opt();
    logmask = lm;
    return count;
}

#ifdef CONFIG_PROC_FS
static int read_users (char *buffer, char **start, off_t offset,
			int count, int *eof, void *data)
{
    off_t pos=0;
    struct client *pcl;
#ifndef __NO_KSPC_CLISTS__
    struct online *ponl;
#endif

    for (pcl = cl_base; pcl; pcl = pcl->next) {
	if (!pcl->masq_port)
	    continue;
	if ((pos += 41) > offset) {
#ifdef __NO_KSPC_CLISTS__
	    buffer += sprintf (buffer, " %10u@%3u.%3u.%3u.%3u:%5u<-%5u\n", pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (pcl->masq_port));
#else
	    buffer += sprintf (buffer, "%c%10u@%3u.%3u.%3u.%3u:%5u<-%5u\n", pcl->online ? GR_LUCORNER : ' ', pcl->uin, NIPQUAD (pcl->cl_addr), ntohs (pcl->cl_port), ntohs (pcl->masq_port));
#endif
	    if (pos >= offset+count)
		goto get_out;
	}
#ifndef __NO_KSPC_CLISTS__
	for (ponl = pcl->online; ponl; ponl = ponl->next)
	    if ((pos += 73) > offset) {
		buffer += sprintf (buffer, "%c%49u@%3u.%3u.%3u.%3u:%5u\n", ponl->next ? GR_VERTICAL : GR_LBCORNER, ponl->uin, NIPQUAD (ponl->addr), ntohs (ponl->port));
		if (pos >= offset+count)
		    goto get_out;
	    }
#endif
    }
    *eof = 1;
get_out:
    *start = buffer + offset - pos;
    return (pos -= offset) > count ? count : pos;
}

static int read_version (char *buffer, char **start, off_t offset,
			 int count, int *eof, void *data)
{
    return sprintf (buffer, "ip_masq_icq-0.56\n");
}

#ifndef __DONT_LOCK__
static int icq_proc_open (struct inode *inode, struct file *file)
{
    MOD_INC_USE_COUNT;
    return 0;
}

static int icq_proc_close (struct inode *inode, struct file *file)
{
    MOD_DEC_USE_COUNT;
    return 0;
}

static struct file_operations icq_proc_file_operations = {
    NULL,		/* lseek -- copy from proc_file_operations */
    NULL,		/* read  -- copy from proc_file_operations */
    NULL,		/* write -- copy from proc_file_operations */
    NULL,		/* readdir */
    NULL,		/* poll    */
    NULL,		/* ioctl   */
    NULL,		/* mmap    */
    icq_proc_open,
    NULL,		/* flush   */
    icq_proc_close
};

static struct inode_operations icq_proc_inode_operations = {
    &icq_proc_file_operations
};
#endif /* !__DONT_LOCK__ */

#define ino_ops NULL
#define declare_proc_entry(opt,type)	proc_##opt = {		\
    0, sizeof (#opt) - 1, #opt, S_IFREG | S_IRUGO | S_IWUSR,	\
    1, 0, 0, 0,		/* nlink, uid, gid, size	 */	\
    ino_ops, NULL, NULL,/* ino_ops, get_info, fill_inode */	\
    NULL, NULL, NULL,	/* next, parent, subdir		 */	\
    &opt, read_##type, write_##type				\
}

static struct proc_dir_entry declare_proc_entry (tcp_timeout, int),
#ifndef __DONT_LOCK__
#undef ino_ops
#define ino_ops &icq_proc_inode_operations
#endif
			     declare_proc_entry (tcp_fin_timeout, int),
			     declare_proc_entry (udp_timeout, int),
			     declare_proc_entry (range, range),
			     declare_proc_entry (log, log),
			     declare_proc_entry (limit, int),
			     declare_proc_entry (hide_local_ip, onoff),
			     declare_proc_entry (hide_remote_local_ip, onoff),
			     declare_proc_entry (intranet, onoff),
			     icq_proc_users = {
				0, 5, "users", S_IFREG | S_IRUSR, 1, 0, 0, 0,
				ino_ops, NULL, NULL, NULL, NULL, NULL, NULL, read_users
			     },
			     icq_proc_version = {
				0, 7, "version", S_IFREG | S_IRUSR, 1, 0, 0, 0,
				ino_ops, NULL, NULL, NULL, NULL, NULL, NULL, read_version
			     },
			     icq_proc_root = {
				0, 3, "icq", S_IFDIR | S_IRUGO | S_IXUGO, 2
			     };

static inline void icq_proc_register (void)
{
    proc_register (&icq_proc_root, &proc_tcp_timeout);	/* empty (NULL) ops field is filled with &proc_file_operations */
#ifndef __DONT_LOCK__
    memcpy (&icq_proc_file_operations, proc_tcp_timeout.ops->default_file_ops, 3*sizeof (void *));
    proc_tcp_timeout.ops = &icq_proc_inode_operations;
#endif
    proc_register (&icq_proc_root, &proc_tcp_fin_timeout);
    proc_register (&icq_proc_root, &proc_udp_timeout);
    proc_register (&icq_proc_root, &proc_range);
    proc_register (&icq_proc_root, &proc_log);
    proc_register (&icq_proc_root, &proc_limit);
    proc_register (&icq_proc_root, &proc_hide_local_ip);
    proc_register (&icq_proc_root, &proc_hide_remote_local_ip);
    proc_register (&icq_proc_root, &proc_intranet);
    proc_register (&icq_proc_root, &icq_proc_users);
    proc_register (&icq_proc_root, &icq_proc_version);
}

static inline void icq_proc_unregister (void)
{
    proc_unregister (&icq_proc_root, proc_tcp_timeout.low_ino);
    proc_unregister (&icq_proc_root, proc_tcp_fin_timeout.low_ino);
    proc_unregister (&icq_proc_root, proc_udp_timeout.low_ino);
    proc_unregister (&icq_proc_root, proc_range.low_ino);
    proc_unregister (&icq_proc_root, proc_log.low_ino);
    proc_unregister (&icq_proc_root, proc_limit.low_ino);
    proc_unregister (&icq_proc_root, proc_hide_local_ip.low_ino);
    proc_unregister (&icq_proc_root, proc_hide_remote_local_ip.low_ino);
    proc_unregister (&icq_proc_root, proc_intranet.low_ino);
    proc_unregister (&icq_proc_root, icq_proc_users.low_ino);
    proc_unregister (&icq_proc_root, icq_proc_users.low_ino);
}
#endif /* CONFIG_PROC_FS */

/*****************************************************************************/

int init_module (void)
{
    int i, ret;

    if (log && write_log (NULL, log, ~0, NULL) == -EINVAL)
	return -EINVAL;

    update_tcp_range();
    update_timeouts();

    if ((ret = register_ip_masq_mod (&icq_mod))) {
	printk (KERN_WARNING "ip_masq_icq: cannot register masq mod\n");
	return ret;
    }
    for (i=0; i < MAX_MASQ_APP_PORTS; i++) {
	if (ports[i]) {
	    if (!(masq_incarnations[i] = kmalloc (sizeof (struct ip_masq_app), GFP_KERNEL))) {
		while (i)
		    kfree (masq_incarnations[--i]);
		unregister_ip_masq_mod (&icq_mod);
		return -ENOMEM;
	    }
	    memcpy (masq_incarnations[i], &ip_masq_icq, sizeof (struct ip_masq_app));
	    if ((ret = register_ip_masq_app (masq_incarnations[i], IPPROTO_UDP, ports[i]))) {
		printk (KERN_WARNING "ip_masq_icq: cannot register support on port %u/UDP\n", ports[i]);
		unregister_ip_masq_mod (&icq_mod);
		return ret;
	    }
	    printk (KERN_INFO "ip_masq_icq: loaded support on port %u/UDP\n", ports[i]);
	} else
	    masq_incarnations[i] = NULL;
    }
#ifdef CONFIG_PROC_FS
    icq_proc_register();
#endif
    return 0;
}

void cleanup_module (void)
{
    int i;

#ifdef CONFIG_PROC_FS
    icq_proc_unregister();
#endif

    for (i=0; i < MAX_MASQ_APP_PORTS; i++) {
	if (masq_incarnations[i]) {
	    if (unregister_ip_masq_app(masq_incarnations[i]))
		printk (KERN_WARNING "ip_masq_icq: cannot unregister support on port %u/UDP\n", ports[i]);
	    else {
		kfree (masq_incarnations[i]);
		masq_incarnations[i] = NULL;
		printk (KERN_INFO "ip_masq_icq: unloaded support on port %u/UDP\n", ports[i]);
	    }
	}
    }
    if (unregister_ip_masq_mod (&icq_mod))
	printk (KERN_WARNING "ip_masq_icq: cannot unregister masq mod\n");
}

/* vim: set ts=8 sw=4: */

