/*
** iplog_scan.c - iplog scan/flood detector.
** Copyright (C) 1999-2001 Ryan McCabe <odin@numb.org>
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License, version 2,
** as published by the Free Software Foundation.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA
**
** $Id: iplog_scan.c,v 1.35 2001/01/01 16:02:14 odin Exp $
*/

#include <config.h>

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <pthread.h>

#include <iplog.h>
#include <iplog_scan.h>
#include <iplog_options.h>

#define ENDSCAN \
	"%s mode expired for %s - received a total of %lu packets (%lu bytes)."

#define ENDSCAN_P \
"%s mode to %s expired for %s - received a total of %lu packets (%lu bytes)."

static size_t st_size;
static struct scan_list *scan_table;

static void log_end_scan(const struct scan_data *cur, u_char type);
static void make_ports_str(u_short, u_char *, size_t, in_port_t *, u_short);
static void scan_cleanup(void *node);

static bool no_res(void) {
	return (false);
}

static const struct scan_info scan[] = {
	{ "TCP: port scan",			tcp_res,	PS_TIMEOUT,		PS_THOLD	},
	{ "TCP: null scan",			tcp_res,	NS_TIMEOUT,		NS_THOLD	},
	{ "TCP: FIN scan",			tcp_res,	FS_TIMEOUT,		FS_THOLD	},
	{ "TCP: SYN scan",			tcp_res,	SS_TIMEOUT,		SS_THOLD	},
	{ "TCP: Xmas scan",			tcp_res,	XS_TIMEOUT,		XS_THOLD	},
	{ "UDP: scan/flood",		udp_res,	UDP_TIMEOUT,	UDP_THOLD	},
	{ "ICMP/UDP: smurf attack",	no_res,		SMURF_TIMEOUT,	SMURF_THOLD	},
	{ "ICMP: ping flood",		icmp_res,	PING_TIMEOUT,	PING_THOLD	}
};

/*
** Log that a scan has expired/ended.
*/

static void log_end_scan(const struct scan_data *cur, u_char type) {
	u_char buf[MAX_HSTLEN];
	struct in_addr in;

	in.s_addr = cur->src_addr;
	host_lookup(&in, scan[type].resolv(), buf, sizeof(buf));

	if (opt_enabled(LOG_DEST)) {
		u_char dbuf[MAX_HSTLEN];

		in.s_addr = cur->dst_addr;
		host_lookup(&in, scan[type].resolv(), dbuf, sizeof(dbuf));

		mysyslog(ENDSCAN_P, scan[type].name, dbuf, buf,
				cur->type[type]->count, cur->type[type]->bytes);
	} else {
		mysyslog(ENDSCAN, scan[type].name, buf,
				cur->type[type]->count, cur->type[type]->bytes);
	}
}

/*
** Initialize the scan hash table.
*/

void init_scan_table(size_t tsize) {
	size_t i;

	st_size = tsize;
	scan_table = xcalloc(st_size, sizeof(struct scan_list));

	for (i = 0 ; i < st_size ; i++)
		pthread_mutex_init(&scan_table[i].lock, NULL);
}

/*
** Fills in "buf" with the ports numbers we have.
*/

static void make_ports_str(	u_short nports, u_char *buf, size_t buflen,
							in_port_t *ports, u_short maxports)
{
	if (nports) {
		if (nports == 1) {
			snprintf(buf, buflen, " [port %d]", ntohs(ports[0]));
		} else {
			u_short idx, slen;

			for (idx = 0 ; idx < nports ; idx++) {
				if (idx == 0)
					slen = snprintf(buf, buflen, " [ports %d", ntohs(ports[0]));
				else
					slen = snprintf(buf, buflen, ",%d", ntohs(ports[idx]));
				buf += slen;
				buflen -= slen;
			}

			if (nports >= maxports)
				xstrncpy(buf, ",...]", buflen);
			else
				xstrncpy(buf, "]", buflen);
		}
	} else
		buf[0] = '\0';
}

/*
** Check whether the host pointed to by "ip" appears to be scanning/flooding
** us.  Returns true if it is, false if it isn't.  If the host isn't found
** in the hash table, a new entry is created for it.
*/

bool check_scan(const struct ip *ip, u_char type, u_long len,
				int sport, int dport)
{
	u_long hash;
	ipaddr_t addr, daddr;
	struct scan_data *cur;
	bool ret = false, found = false, log_scan = false;
	/*
	** 6 chars for each port number "nnnnn," + 15 of
	** padding " [ports ", "...]", ...
	*/
	u_char bsports[SCAN_SRC_PORTS * 6 + 15];
	u_char bdports[SCAN_DST_PORTS * 6 + 15];

	daddr = ip->ip_dst.s_addr;
	addr = ip->ip_src.s_addr;

	if (type == SCAN_SMURF && opt_enabled(SMURF))
		addr &= 0xffffff;

	hash = SCAN_HASH(addr, daddr, st_size);

	pthread_mutex_lock(&scan_table[hash].lock);
	for (cur = scan_table[hash].head ; cur != NULL ; cur = cur->next) {
		if (cur->src_addr == addr && cur->dst_addr == daddr) {
			struct scan_t *data;
			time_t cur_time;

			found = true;

			if (cur->type[type] == NULL)
				cur->type[type] = xcalloc(1, sizeof(struct scan_t));

			data = cur->type[type];

			time(&cur_time);
			cur->last = cur_time;
			data->expire = cur_time + scan[type].timeout;
			data->bytes += len;

			if (sport != -1 && data->sports_count < SCAN_SRC_PORTS) {
				u_short idx;

				/* Search for this port in previous ports. */
				idx = 0;
				while (idx < data->sports_count && data->sports[idx] != sport)
					++idx;
				if (idx == data->sports_count)
					data->sports[data->sports_count++] = sport;
			}

			if (dport != -1 && data->dports_count < SCAN_DST_PORTS) {
				u_short idx;

				/* Search for this port in previous ports. */
				idx = 0;
				while (idx < data->dports_count && data->dports[idx] != dport)
					++idx;
				if (idx == data->dports_count)
					data->dports[data->dports_count++] = dport;
			}

			if (++data->count >= scan[type].threshold) {
				ret = true;
				if (data->logged == false) {
					data->logged = true;
					log_scan = true;
					make_ports_str(data->sports_count, bsports,
						sizeof(bsports), data->sports, SCAN_SRC_PORTS);
					make_ports_str(data->dports_count, bdports,
						sizeof(bdports), data->dports, SCAN_DST_PORTS);
				}
			}

			break;
		}
	}
	pthread_mutex_unlock(&scan_table[hash].lock);

	if (found == false) {
		struct scan_data *new_entry = xcalloc(1, sizeof(struct scan_data));
		struct scan_t *data;
		time_t cur_time;

		pthread_mutex_lock(&scan_table[hash].lock);
		if (scan_table[hash].cnt >= SCAN_MAXENT) {
			/*
			** This bucket is full.  Evict the oldest entry.
			*/
			size_t j;
			u_long oldest_time = ~0;
			struct scan_data *oldest = NULL, *tcur;

			for (tcur = scan_table[hash].head ; tcur ; tcur = tcur->next) {
				if ((u_long) tcur->last <= oldest_time) {
					oldest_time = tcur->last;
					oldest = tcur;
				}
			}

			for (j = 0 ; j < SCAN_TOTAL ; j++) {
				if (oldest->type[j] != NULL) {
					if (oldest->type[j]->count >= scan[j].threshold)
						log_end_scan(oldest, j);
					xfree(oldest->type[j]);
				}
			}

			cur = dlist_delete(oldest, &scan_table[hash].head);
			scan_table[hash].cnt--;
		}
		pthread_mutex_unlock(&scan_table[hash].lock);

		new_entry->src_addr = addr;
		new_entry->dst_addr = daddr;
		new_entry->type[type] = xcalloc(1, sizeof(struct scan_t));
		data = new_entry->type[type];
		data->bytes = len;
		++data->count; /* = 1  */

		if (sport != -1) {
			++data->sports_count; /* = 1 */
			data->sports[0] = sport;
		}

		if (dport != -1) {
			++data->dports_count; /* = 1 */
			data->dports[0] = dport;
		}

		time(&cur_time);
		data->expire = cur_time + scan[type].timeout;
		new_entry->last = cur_time;

		pthread_mutex_lock(&scan_table[hash].lock);
		dlist_prepend(new_entry, &scan_table[hash].head);
		++scan_table[hash].cnt;
		pthread_mutex_unlock(&scan_table[hash].lock);
	}

	if (log_scan == true) {
		u_char buf[MAX_HSTLEN];
		struct in_addr in;

		in.s_addr = addr;
		host_lookup(&in, scan[type].resolv(), buf, sizeof(buf));

		if (opt_enabled(LOG_DEST)) {
			u_char dbuf[MAX_HSTLEN];

			host_lookup(&ip->ip_dst, scan[type].resolv(), dbuf, sizeof(dbuf));
			mysyslog("%s detected to %s%s from %s%s", scan[type].name, dbuf,
				 bdports, buf, bsports);
		} else
			mysyslog("%s detected%s from %s%s", scan[type].name, bdports, buf,
				 bsports);
		ret = true;
	}

	return (ret);
}

/*
** Delete entries from the hash table that have expired.
*/

void expire_scans(void) {
	size_t i, j;
	struct scan_data *cur;
	bool remove_entry;

	for (i = 0 ; i < st_size ; i++) {
		pthread_mutex_lock(&scan_table[i].lock);
		cur = scan_table[i].head;

		for (remove_entry = true ; cur != NULL ;) {
			for (j = 0 ; j < SCAN_TOTAL ; j++) {
				if (cur->type[j] == NULL)
					continue;
				if (time(NULL) >= cur->type[j]->expire) {
					if (cur->type[j]->count >= scan[j].threshold)
						log_end_scan(cur, j);
					xfree(cur->type[j]);
				} else
					remove_entry = false;
			}

			if (remove_entry == true) {
				cur = dlist_delete(cur, &scan_table[i].head);
				--scan_table[i].cnt;
			} else
				cur = cur->next;
		}

		pthread_mutex_unlock(&scan_table[i].lock);
	}
}

#ifdef HAVE_PTHREAD_CANCEL

/*
** Destroys the scan hash table.
*/

void destroy_scan_table(void) {
	size_t i;

	for (i = 0 ; i < st_size ; i++) {
		if (scan_table[i].head != NULL)
			dlist_destroy(scan_table[i].head, scan_cleanup);
		memset(&scan_table[i], 0, sizeof(struct scan_list));
		pthread_mutex_init(&scan_table[i].lock, NULL);
	}
}

/*
** Cleanup function called when a scan table entry is deleted.
*/

static void scan_cleanup(void *node) {
	struct scan_data *cur = (struct scan_data *) node;
	size_t i;

	for (i = 0 ; i < SCAN_TOTAL ; i++) {
		if (cur->type[i] != NULL)
			xfree(cur->type[i]);
	}
}
#endif

/* vim:ts=4:sw=8:tw=0 */


syntax highlighted by Code2HTML, v. 0.9.1