view src/syslogconfig.cpp @ 31:601bc0e075e1

gpl3
author carl
date Sun, 09 Sep 2007 10:59:15 -0700
parents 6465d8640489
children d2ceebcf6595
line wrap: on
line source

/***************************************************************************
 *	 Copyright (C) 2005 by 510 Software Group							   *
 *																		   *
 *																		   *
 *	 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.								   *
 *																		   *
 *	 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.			   *
 ***************************************************************************/

#include "includes.h"
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <limits.h>

static char* syslogconfig_version = "$Id$";

char *token_add;
char *token_bucket;
char *token_file;
char *token_ignore;
char *token_include;
char *token_index;
char *token_lbrace;
char *token_pattern;
char *token_rbrace;
char *token_remove;
char *token_semi;
char *token_slash;
char *token_threshold;

struct ltint
{
  bool operator()(const int s1, const int s2) const
  {
	return (unsigned)s1 < (unsigned)s2;
  }
};

struct bucket {
	int  count;
	bool latch; // true iff ever count>threshold
};

string_set	all_strings;	// owns all the strings, only modified by the config loader thread
const int maxlen = 1000;	// used for snprintf buffers
typedef map<int, bucket, ltint>   ip_buckets;

class IPR {
	ip_buckets	violations;
public:
	void add(int ip, int amount, CONFIG &con, char *file_name, int pattern_index);
	void leak(int amount, CONFIG &con);
	void update(int ip, bool added, char *file_name, int pattern_index);
	void changed(CONFIG &con, int ip, bool added);
};

IPR recorder;


////////////////////////////////////////////////
//
void IPR::add(int ip, int amount, CONFIG &con, char *file_name, int pattern_index) {
	if (con.looking(ip)) {
		ip_buckets::iterator i = violations.find(ip);
		if (i == violations.end()) {
			bucket b;
			b.count = amount;
			b.latch = (con.get_threshold() <= b.count);
			violations[ip] = b;
			if (b.latch) {
				update(ip, true, file_name, pattern_index);
				changed(con, ip, true);
			}
		}
		else {
			bucket &b = (*i).second;
			if (b.count < (INT_MAX-amount)) {
				int t = con.get_threshold();
				int c = b.count;
				b.count += amount;
				if ((!b.latch) && (c < t) && (t <= b.count)) {
					b.latch = true;
					update(ip, true, file_name, pattern_index);
					changed(con, ip, true);
				}
			}
		}
	}
}


void IPR::leak(int amount, CONFIG &con) {
	for (ip_buckets::iterator i=violations.begin(); i!=violations.end(); ) {
		int    ip = (*i).first;
		bucket &b = (*i).second;
		if (b.count <= amount) {
			if (b.latch) {
				update(ip, false, NULL, 0);
				changed(con, ip, false);
			}
			violations.erase(i++);
		}
		else {
			b.count -= amount;
			i++;
		}
	}
}


void IPR::update(int ip, bool added, char *file_name, int pattern_index) {
	if (debug_syslog > 2) {
		char buf[maxlen];
		in_addr ad;
		ad.s_addr = htonl(ip);
		if (added) snprintf(buf, maxlen, "dropping traffic from/to %s based on pattern match %d in %s", inet_ntoa(ad), pattern_index, file_name);
		else	   snprintf(buf, maxlen, "allowing traffic from/to %s", inet_ntoa(ad));
		my_syslog(buf);
	}
}


void IPR::changed(CONFIG &con, int ip, bool added) {
	int t = con.get_threshold();
	char buf[maxlen];
	if (added) {
		bucket &b = violations[ip];
		if (con.looking(ip) && (b.count > t)) {
			in_addr ad;
			ad.s_addr = htonl(ip);
			snprintf(buf, maxlen, con.add_command, inet_ntoa(ad));
			system(buf);
		}
	}
	else {
		in_addr ad;
		ad.s_addr = htonl(ip);
		snprintf(buf, maxlen, con.remove_command, inet_ntoa(ad));
		system(buf);
	}
}


////////////////////////////////////////////////
//
int ip_address(char *have);
int ip_address(char *have) {
	int ipaddr = 0;
	in_addr ip;
	if (inet_aton(have, &ip)) ipaddr = ip.s_addr;
	else {
		struct hostent *host = gethostbyname(have);
		if (host && host->h_addrtype == AF_INET) memcpy(&ipaddr, host->h_addr, sizeof(ipaddr));
	}
	return ntohl(ipaddr);
}


////////////////////////////////////////////////
//
PATTERN::PATTERN(TOKEN &tok, char *pattern_, int index_, int amount_) {
	pattern = pattern_;
	index	= index_;
	amount	= amount_;
	if (pattern) {
		int rc = regcomp(&re, pattern, REG_ICASE | REG_EXTENDED);
		if (rc) {
			char bu[maxlen];
			regerror(rc, &re, bu, maxlen);
			char buf[maxlen];
			snprintf(buf, sizeof(buf), "pattern %s not valid - %s", pattern, bu);
			tok.token_error(buf);
			pattern = NULL;
		}
	}
}


PATTERN::~PATTERN() {
	regfree(&re);
}


bool PATTERN::process(char *buf, CONFIG &con, char *file_name, int pattern_index) {
	if (pattern) {
		const int nmatch = index+1;
		regmatch_t match[nmatch];
		if (0 == regexec(&re, buf, nmatch, match, 0)) {
			int s = match[index].rm_so;
			int e = match[index].rm_eo;
			if (s != -1) {
				if (debug_syslog > 3) {
					my_syslog(buf); // show lines with matches
				}
				buf[e] = '\0';
				int ip = ip_address(buf+s);
				if (ip) {
					recorder.add(ip, amount, con, file_name, pattern_index);
				}
				return true;
			}
		}
	}
	return false;
}


void PATTERN::dump(int level) {
	char indent[maxlen];
	int i = min(maxlen-1, level*4);
	memset(indent, ' ', i);
	indent[i] = '\0';
	printf("%s pattern \"%s\" {; \n", indent, pattern);
	printf("%s     index %d; \n", indent, index);
	printf("%s     bucket %d; \n", indent, amount);
	printf("%s }; \n", indent);
}


////////////////////////////////////////////////
//
CONFIG::CONFIG() {
	reference_count    = 0;
	generation		   = 0;
	load_time		   = 0;
	threshold		   = 500;
	add_command 	   = "/sbin/iptables -I INPUT --src %s --jump DROP";
	remove_command	   = "/sbin/iptables -D INPUT --src %s --jump DROP";
}


CONFIG::~CONFIG() {
	for (syslogconfig_list::iterator i=syslogconfigs.begin(); i!=syslogconfigs.end(); i++) {
		SYSLOGCONFIG *c = *i;
		delete c;
	}
	ignore.clear();
}


void CONFIG::add_syslogconfig(SYSLOGCONFIGP con) {
	syslogconfigs.push_back(con);
}


void CONFIG::add_pair(IPPAIR pair) {
	ignore.push_back(pair);
}


void CONFIG::dump() {
	printf(" threshold %d; \n\n", threshold);

	printf(" add_command \"%s\"; \n",      add_command);
	printf(" remove_command \"%s\"; \n\n", remove_command);

	printf(" ignore { \n");
	for (ippair_list::iterator i=ignore.begin(); i!=ignore.end(); i++) {
		IPPAIR &p = *i;
		in_addr ip;
		ip.s_addr = htonl(p.first);
		printf("     %s/%d; \n", inet_ntoa(ip), p.cidr);
	}
	printf(" }; \n\n");

	for (syslogconfig_list::iterator i=syslogconfigs.begin(); i!=syslogconfigs.end(); i++) {
		SYSLOGCONFIGP c = *i;
		c->dump(0);
	}
}


void CONFIG::read() {
	while (true) {
		bool have = false;
		for (syslogconfig_list::iterator i=syslogconfigs.begin(); i!=syslogconfigs.end(); i++) {
			SYSLOGCONFIGP c = *i;
			have |= c->read(*this);
		}
		if (!have) break;
	}
}


void CONFIG::sleep(int duration, time_t &previous) {
	::sleep(duration);
	time_t now = time(NULL);
	recorder.leak(now-previous, *this);
	previous = now;
}


bool CONFIG::looking(int ip) {
	for (ippair_list::iterator i=ignore.begin(); i!=ignore.end(); i++) {
		IPPAIR &p = *i;
		if ((p.first <= ip) && (ip <= p.last)) return false;
	}
	return true;
}

////////////////////////////////////////////////
//
SYSLOGCONFIG::SYSLOGCONFIG(TOKEN &tok, char *file_name_) {
	tokp	  = &tok;
	file_name = file_name_;
	open(true);
}


SYSLOGCONFIG::~SYSLOGCONFIG() {
	close();
	for (pattern_list::iterator i=patterns.begin(); i!=patterns.end(); i++) {
		PATTERN *p = *i;
		delete p;
	}
}


void SYSLOGCONFIG::open(bool msg) {
	fd		  = ::open(file_name, O_RDONLY);
	len 	  = 0;
	if (fd == -1) {
		if (msg) {
			char buf[maxlen];
			snprintf(buf, sizeof(buf), "syslog file %s not readable", file_name);
			tokp->token_error(buf);
		}
	}
	else {
		if (debug_syslog > 1) {
			snprintf(buf, sizeof(buf), "syslog file %s opened", file_name);
			my_syslog(buf);
		}
		lseek(fd, 0, SEEK_END);
		if (fstat(fd, &openfdstat)) {
			close();
			snprintf(buf, sizeof(buf), "syslog file %s cannot stat after open", file_name);
			tokp->token_error(buf);
		}
		// specify that this fd gets closed on exec, so that selinux
		// won't complain about iptables trying to read log files.
		int oldflags = fcntl(fd, F_GETFD, 0);
		if (oldflags >= 0) {
			fcntl(fd, F_SETFD, oldflags | FD_CLOEXEC);
		}
	}
}


bool SYSLOGCONFIG::read(CONFIG &con) {
	if (failed()) {
		open(false);
		if (failed()) return false;
	}
	int n = ::read(fd, buf+len, buflen-len);
	bool have = (n > 0);
	if (have) {
		len += n;
		while (true) {
			char *p = (char*)memchr(buf, '\n', len);
			if (!p) break;
			n = p-buf;
			*p = '\0';
			process(con);		// process null terminated string
			len -= n+1;
			memmove(buf, p+1, len);
		}
		// no <lf> in a full buffer
		if (len == buflen) len = 0;
	}
	else {
		// check for file close
		struct stat filenamest;
		if (0 == stat(file_name, &filenamest)) {
			if ((filenamest.st_dev != openfdstat.st_dev) ||
				(filenamest.st_ino != openfdstat.st_ino)) {
				close();
			}
		}
		else {
			// filename no longer exists
			close();
		}
	}
	return have;
}


void SYSLOGCONFIG::close() {
	if (debug_syslog > 1) {
		snprintf(buf, sizeof(buf), "syslog file %s closed", file_name);
		my_syslog(buf);
	}
	if (fd != -1) ::close(fd);
	fd = -1;
}


void SYSLOGCONFIG::add_pattern(PATTERNP pat) {
	patterns.push_back(pat);
}


void SYSLOGCONFIG::process(CONFIG &con) {
	int pi=0;
	for (pattern_list::iterator i=patterns.begin(); i!=patterns.end(); i++) {
		PATTERN *p = *i;
		if (p->process(buf, con, file_name, pi)) break;
		pi++;
	}
}


void SYSLOGCONFIG::dump(int level) {
	char indent[maxlen];
	int i = min(maxlen-1, level*4);
	memset(indent, ' ', i);
	indent[i] = '\0';
	char buf[maxlen];
	printf("%s file \"%s\" {\n", indent, file_name);
	for (pattern_list::iterator i=patterns.begin(); i!=patterns.end(); i++) {
		PATTERN *p = *i;
		p->dump(level+1);
	}
	printf("%s }; \n", indent);
}


////////////////////////////////////////////////
// helper to discard the strings held by a string_set
//
void discard(string_set &s) {
	for (string_set::iterator i=s.begin(); i!=s.end(); i++) {
		free(*i);
	}
	s.clear();
}


////////////////////////////////////////////////
// helper to register a string in a string set
//
char* register_string(string_set &s, char *name) {
	string_set::iterator i = s.find(name);
	if (i != s.end()) return *i;
	char *x = strdup(name);
	s.insert(x);
	return x;
}


////////////////////////////////////////////////
// register a global string
//
char* register_string(char *name) {
	return register_string(all_strings, name);
}


////////////////////////////////////////////////
//
bool tsa(TOKEN &tok, char *token);
bool tsa(TOKEN &tok, char *token) {
	char *have = tok.next();
	if (have == token) return true;
	tok.token_error(token, have);
	return false;
}


////////////////////////////////////////////////
//
bool parse_pattern(TOKEN &tok, SYSLOGCONFIG &con);
bool parse_pattern(TOKEN &tok, SYSLOGCONFIG &con) {
	char *pat = tok.next();
	int  ind, buc;
	if (!tsa(tok, token_lbrace)) return false;
	while (true) {
		char *have = tok.next();
		if (!have) break;
		if (have == token_rbrace) break;
		if (have == token_index) {
			have = tok.next();
			ind  = atoi(have);
			if (!tsa(tok, token_semi)) return false;
		}
		else if (have == token_bucket) {
			have = tok.next();
			buc  = atoi(have);
			if (!tsa(tok, token_semi)) return false;
		}
		else {
			tok.token_error("index/bucket", have);
			return false;
		}
	}
	if (!tsa(tok, token_semi)) return false;
	PATTERNP patt = new PATTERN(tok, pat, ind, buc);
	con.add_pattern(patt);
	return true;
}


////////////////////////////////////////////////
//
bool parse_ignore(TOKEN &tok, CONFIG &dc);
bool parse_ignore(TOKEN &tok, CONFIG &dc) {
	if (!tsa(tok, token_lbrace)) return false;
	while (true) {
		char *have = tok.next();
		if (!have) break;
		if (have == token_rbrace) break;
		int ipaddr = ip_address(have);
		if (ipaddr == 0) {
			tok.token_error("ip address", have);
			return false;
		}
		if (!tsa(tok, token_slash)) return false;
		have = tok.next();
		int mask = atoi(have);
		if ((mask < 8) || (mask > 32)) {
			tok.token_error("cidr 8..32 value", have);
			return false;
		}
		if (!tsa(tok, token_semi)) return false;
		IPPAIR pair;
		const int masks[33] = {0xffffffff,	//	 0
							   0x7fffffff,	//	 1
							   0x3fffffff,	//	 2
							   0x1fffffff,	//	 3
							   0x0fffffff,	//	 4
							   0x07ffffff,	//	 5
							   0x03ffffff,	//	 6
							   0x01ffffff,	//	 7
							   0x00ffffff,	//	 8
							   0x007fffff,	//	 9
							   0x003fffff,	//	10
							   0x001fffff,	//	11
							   0x000fffff,	//	12
							   0x0007ffff,	//	13
							   0x0003ffff,	//	14
							   0x0001ffff,	//	15
							   0x0000ffff,	//	16
							   0x00007fff,	//	17
							   0x00003fff,	//	18
							   0x00001fff,	//	19
							   0x00000fff,	//	20
							   0x000007ff,	//	21
							   0x000003ff,	//	22
							   0x000001ff,	//	23
							   0x000000ff,	//	24
							   0x0000007f,	//	25
							   0x0000003f,	//	26
							   0x0000001f,	//	27
							   0x0000000f,	//	28
							   0x00000007,	//	29
							   0x00000003,	//	30
							   0x00000001,	//	31
							   0x00000000}; //	32
		pair.first = ipaddr;
		pair.last  = ipaddr | masks[mask];
		pair.cidr  = mask;
		dc.add_pair(pair);
	}
	if (!tsa(tok, token_semi)) return false;
	return true;
}


////////////////////////////////////////////////
//
bool parse_syslogconfig(TOKEN &tok, CONFIG &dc);
bool parse_syslogconfig(TOKEN &tok, CONFIG &dc) {
	char *name = tok.next();
	if (!tsa(tok, token_lbrace)) return false;
	SYSLOGCONFIGP con = new SYSLOGCONFIG(tok, name);
	if (con->failed()) {
		delete con;
		return false;
	}
	dc.add_syslogconfig(con);
	while (true) {
		char *have = tok.next();
		if (!have) break;
		if (have == token_rbrace) break;
		if (have == token_pattern) {
			if (!parse_pattern(tok, *con)) return false;
		}
		else {
			tok.token_error("pattern", have);
			return false;
		}
	}
	if (!tsa(tok, token_semi)) return false;
	return true;
}


////////////////////////////////////////////////
// parse a config file
//
bool load_conf(CONFIG &dc, char *fn) {
	int count = 0;
	TOKEN tok(fn, &dc.config_files);
	while (true) {
		char *have = tok.next();
		if (!have) break;
		if (have == token_threshold) {
			have = tok.next();
			dc.set_threshold(atoi(have));
			if (!tsa(tok, token_semi)) return false;
		}
		else if (have == token_ignore) {
			if (!parse_ignore(tok, dc)) return false;
		}
		else if (have == token_add) {
			have = tok.next();
			dc.set_add(have);
			if (!tsa(tok, token_semi)) return false;
		}
		else if (have == token_remove) {
			have = tok.next();
			dc.set_remove(have);
			if (!tsa(tok, token_semi)) return false;
		}
		else if (have == token_file) {
			if (!parse_syslogconfig(tok, dc)) return false;
			count++;
		}
		else {
			tok.token_error("threshold/ignore/add_command/remove_command/file", have);
			return false;
		}
	}
	tok.token_error("load_conf() found %d syslog files in %s", count, fn);
	return (!dc.syslogconfigs.empty());
}


////////////////////////////////////////////////
// init the tokens
//
void token_init() {
	token_add		 = register_string("add_command");
	token_bucket	 = register_string("bucket");
	token_file		 = register_string("file");
	token_ignore	 = register_string("ignore");
	token_include	 = register_string("include");
	token_index 	 = register_string("index");
	token_lbrace	 = register_string("{");
	token_pattern	 = register_string("pattern");
	token_rbrace	 = register_string("}");
	token_remove	 = register_string("remove_command");
	token_semi		 = register_string(";");
	token_slash 	 = register_string("/");
	token_threshold  = register_string("threshold");
}