Mercurial > syslog2iptables
view src/syslogconfig.cpp @ 79:831d0b46bbd2 stable-1-0-18
add more logging when blocked addresses move to higher scale values
author | Carl Byington <carl@five-ten-sg.com> |
---|---|
date | Wed, 15 Jul 2020 14:22:37 -0700 |
parents | c6c8a2102a3e |
children |
line wrap: on
line source
/* Copyright (c) 2007 Carl Byington - 510 Software Group, released under the GPL version 3 or any later version at your choice available at http://www.gnu.org/licenses/gpl-3.0.txt */ #include "includes.h" #include <fcntl.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <limits.h> const char *token_add; const char *token_bucket; const char *token_context; const char *token_file; const char *token_ignore; const char *token_include; const char *token_index; const char *token_lbrace; const char *token_message; const char *token_pattern; const char *token_rbrace; const char *token_remove; const char *token_semi; const char *token_slash; const char *token_threshold; string_set all_strings;// owns all the strings, only modified by the config loader thread recorder_map recorders; // all the recorders are named by their context const int maxlen = 1000; // used for snprintf buffers const int scale_max = 500000; //////////////////////////////////////////////// // IPR::IPR() { reference_count = 0; daily_timer = 86400; } IPR* IPR::find(const char* name) { recorder_map::iterator m = recorders.find(name); if (m == recorders.end()) recorders[name] = new IPR; recorders[name]->reference(1); return recorders[name]; } void IPR::release(const char* name) { recorder_map::iterator m = recorders.find(name); IPR* i = (*m).second; int r = i->reference(-1); if (r == 0) { delete i; recorders.erase(m); } } void IPR::add(int ip, int amount, CONTEXT &con, const char *file_name, int pattern_index, const char *message) { if (con.looking(ip)) { if (amount > 0) { int original_amount = amount; ip_buckets::iterator j = repeat_offenders.find(ip); int scale = (j == repeat_offenders.end()) ? 1 : (*j).second.count; amount *= scale; ip_buckets::iterator i = violations.find(ip); if (i == violations.end()) { bucket b; b.count = amount; b.blocked = (con.get_threshold() <= b.count); b.max_scale = 1; violations[ip] = b; if (b.blocked) { update(ip, true, scale, file_name, pattern_index, message); changed(con, ip, true); } } else { bucket &b = (*i).second; if ((b.count >= 0) && (b.count < 2600000)) { // good authentication (count<0) prevents blocking // not much point in blocking for more than a month b.count += amount; int threshold = con.get_threshold(); if (b.blocked) { int effective_scale = 2; while (original_amount * effective_scale < b.count) { effective_scale = effective_scale * 3 / 2; } if (effective_scale > b.max_scale) { b.max_scale = effective_scale; if (debug_syslog > 2) { char buf[maxlen]; in_addr ad; ad.s_addr = htonl(ip); if (message) snprintf(buf, maxlen, "upgrade dropping traffic from/to %s based on %s in %s, scale %d", inet_ntoa(ad), message, file_name, effective_scale); else snprintf(buf, maxlen, "upgrade dropping traffic from/to %s based on pattern match %d in %s, scale %d", inet_ntoa(ad), pattern_index, file_name, effective_scale); my_syslog(buf); } } } if ((!b.blocked) && (threshold <= b.count)) { b.blocked = true; update(ip, true, scale, file_name, pattern_index, message); changed(con, ip, true); } } } } else if (amount < 0) { char buf[maxlen]; in_addr ad; ad.s_addr = htonl(ip); snprintf(buf, maxlen, "%s for %s", message, inet_ntoa(ad)); my_syslog(buf); ip_buckets::iterator j = repeat_offenders.find(ip); if (j != repeat_offenders.end()) { repeat_offenders.erase(j++); snprintf(buf, maxlen, "removing %s from repeat offenders", inet_ntoa(ad)); my_syslog(buf); } ip_buckets::iterator i = violations.find(ip); if (i == violations.end()) { bucket b; b.count = amount; b.blocked = false; b.max_scale = 1; violations[ip] = b; } else { bucket &b = (*i).second; b.count = amount; if (b.blocked) { update(ip, false, 0, NULL, 0, NULL); changed(con, ip, false); } } } } } void IPR::leak(int amount, CONTEXT &con) { for (ip_buckets::iterator i=violations.begin(); i!=violations.end(); ) { int ip = (*i).first; bucket &b = (*i).second; if (b.count < 0) { if (b.count >= -amount) violations.erase(i++); else { b.count += amount; i++; } } else { if (b.count <= amount) { if (b.blocked) { update(ip, false, 0, NULL, 0, NULL); changed(con, ip, false); } violations.erase(i++); } else { b.count -= amount; i++; } } } daily_timer -= amount; if (daily_timer < 0) { daily_timer = 86400; for (ip_buckets::iterator j=repeat_offenders.begin(); j!=repeat_offenders.end(); ) { int ip = (*j).first; bucket &b = (*j).second; b.count = b.count * 2 / 3; if (b.count <= 2) { repeat_offenders.erase(j++); char buf[maxlen]; in_addr ad; ad.s_addr = htonl(ip); snprintf(buf, maxlen, "removing %s from repeat offenders", inet_ntoa(ad)); my_syslog(buf); } else { j++; } } } } void IPR::free_all(CONTEXT &con) { if (debug_syslog > 2) { my_syslog("syslog2iptables shutting down"); } for (ip_buckets::iterator i=violations.begin(); i!=violations.end(); i++) { int ip = (*i).first; bucket &b = (*i).second; if (b.blocked) { update(ip, false, 0, NULL, 0, NULL); changed(con, ip, false); } } violations.clear(); } void IPR::update(int ip, bool added, int scale, const char *file_name, int pattern_index, const char *message) { char buf[maxlen]; in_addr ad; ad.s_addr = htonl(ip); if (added) { if (debug_syslog > 2) { if (message) snprintf(buf, maxlen, "dropping traffic from/to %s based on %s in %s, scale %d", inet_ntoa(ad), message, file_name, scale); else snprintf(buf, maxlen, "dropping traffic from/to %s based on pattern match %d in %s, scale %d", inet_ntoa(ad), pattern_index, file_name, scale); my_syslog(buf); } ip_buckets::iterator j = repeat_offenders.find(ip); if (j == repeat_offenders.end()) { bucket b; b.count = 2; b.blocked = true; // unused b.max_scale = 1; // unused repeat_offenders[ip] = b; } else { bucket &b = (*j).second; if (b.count < scale_max) b.count = b.count * 3 / 2; } } else { if (debug_syslog > 2) { snprintf(buf, maxlen, "allowing traffic from/to %s", inet_ntoa(ad)); my_syslog(buf); } } } void IPR::changed(CONTEXT &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(const char *have); int ip_address(const 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, const char *pattern_, int index_, int amount_, const char *msg_) { pattern = pattern_; index = index_; amount = amount_; message = msg_; 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, CONTEXT &con, const 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) { con.recorder->add(ip, amount, con, file_name, pattern_index, message); } 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); if (message) printf("%s message \"%s\"; \n", indent, message); printf("%s }; \n", indent); } //////////////////////////////////////////////// // CONTEXT::CONTEXT(const char *nam) { name = nam; threshold = 500; add_command = "/sbin/iptables -I INPUT --src %s --jump DROP"; remove_command = "/sbin/iptables -D INPUT --src %s --jump DROP"; recorder = IPR::find(name); } //////////////////////////////////////////////// // CONTEXT::~CONTEXT() { ignore.clear(); for (syslogconfig_list::iterator i=syslogconfigs.begin(); i!=syslogconfigs.end(); i++) { SYSLOGCONFIG *c = *i; delete c; } IPR::release(name); } void CONTEXT::add_syslogconfig(SYSLOGCONFIGP con) { syslogconfigs.push_back(con); } void CONTEXT::add_pair(IPPAIR pair) { ignore.push_back(pair); } void CONTEXT::dump() { string indents(" "); const char *indent = indents.c_str(); printf("context %s {\n", name); printf("%s threshold %d; \n\n", indent, threshold); printf("%s add_command \"%s\"; \n", indent, add_command); printf("%s remove_command \"%s\"; \n\n", indent, remove_command); printf("%s ignore { \n", indent); 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 %s/%d; \n", indent, inet_ntoa(ip), p.cidr); } printf("%s }; \n\n", indent); for (syslogconfig_list::iterator i=syslogconfigs.begin(); i!=syslogconfigs.end(); i++) { SYSLOGCONFIGP c = *i; c->dump(1); } printf("}; \n\n"); } void CONTEXT::read(CONFIG &con) { 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 CONTEXT::free_all() { recorder->free_all(*this); } void CONTEXT::leak(int delta) { recorder->leak(delta, *this); } bool CONTEXT::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; } //////////////////////////////////////////////// // CONFIG::CONFIG() { reference_count = 0; generation = 0; load_time = 0; } CONFIG::~CONFIG() { for (context_list::iterator i=contexts.begin(); i!=contexts.end(); i++) { CONTEXT *c = *i; delete c; } } void CONFIG::dump() { for (context_list::iterator i=contexts.begin(); i!=contexts.end(); i++) { CONTEXTP c = *i; c->dump(); } } void CONFIG::read() { for (context_list::iterator i=contexts.begin(); i!=contexts.end(); i++) { CONTEXT *c = *i; c->read(*this); } } void CONFIG::sleep(int duration, time_t &previous) { ::sleep(duration); time_t now = time(NULL); for (context_list::iterator i=contexts.begin(); i!=contexts.end(); i++) { CONTEXT *c = *i; c->leak(now-previous); } previous = now; } void CONFIG::free_all() { for (context_list::iterator i=contexts.begin(); i!=contexts.end(); i++) { CONTEXT *c = *i; c->free_all(); } } //////////////////////////////////////////////// // SYSLOGCONFIG::SYSLOGCONFIG(TOKEN &tok, const 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); } if (msg) 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(CONTEXT &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(CONTEXT &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'; 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((void*)*i); } s.clear(); } //////////////////////////////////////////////// // helper to register a string in a string set // const char* register_string(string_set &s, const char *name) { string_set::const_iterator i = s.find(name); if (i != s.end()) return *i; char *x = strdup(name); s.insert(x); return x; } //////////////////////////////////////////////// // register a global string // const char* register_string(const char *name) { return register_string(all_strings, name); } //////////////////////////////////////////////// // clear all global strings, helper for valgrind checking // void clear_strings() { discard(all_strings); } //////////////////////////////////////////////// // bool tsa(TOKEN &tok, const char *token); bool tsa(TOKEN &tok, const char *token) { const char *have = tok.next(); if (have == token) return true; tok.token_error(token, have); return false; } //////////////////////////////////////////////// // bool parse_pattern(TOKEN &tok, SYSLOGCONFIG &con, CONTEXT &me); bool parse_pattern(TOKEN &tok, SYSLOGCONFIG &con, CONTEXT &me) { const char *pat = tok.next(); int ind = 0; int buc = 0; const char *msg = NULL; if (!tsa(tok, token_lbrace)) return false; while (true) { const 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 if (have == token_message) { msg = tok.next(); 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, msg); con.add_pattern(patt); return true; } //////////////////////////////////////////////// // bool parse_ignore(TOKEN &tok, CONFIG &dc, CONTEXT &me); bool parse_ignore(TOKEN &tok, CONFIG &dc, CONTEXT &me) { if (!tsa(tok, token_lbrace)) return false; while (true) { const 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 unsigned 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 | (int)masks[mask]; pair.cidr = mask; me.add_pair(pair); } if (!tsa(tok, token_semi)) return false; return true; } //////////////////////////////////////////////// // bool parse_syslogconfig(TOKEN &tok, CONFIG &dc, CONTEXT &me); bool parse_syslogconfig(TOKEN &tok, CONFIG &dc, CONTEXT &me) { const char *name = tok.next(); if (!tsa(tok, token_lbrace)) return false; SYSLOGCONFIGP con = new SYSLOGCONFIG(tok, name); if (con->failed()) { delete con; return false; } me.add_syslogconfig(con); while (true) { const char *have = tok.next(); if (!have) break; if (have == token_rbrace) break; if (have == token_pattern) { if (!parse_pattern(tok, *con, me)) return false; } else { tok.token_error("pattern", have); return false; } } if (!tsa(tok, token_semi)) return false; return true; } //////////////////////////////////////////////// // bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent); bool parse_context(TOKEN &tok, CONFIG &dc, CONTEXTP parent) { const char *name = tok.next(); if (!tsa(tok, token_lbrace)) return false; CONTEXTP con = new CONTEXT(name); while (true) { const char *have = tok.next(); if (!have) break; if (have == token_rbrace) break; // done if (have == token_threshold) { have = tok.next(); con->set_threshold(atoi(have)); if (!tsa(tok, token_semi)) return false; } else if (have == token_ignore) { if (!parse_ignore(tok, dc, *con)) return false; } else if (have == token_add) { have = tok.next(); con->set_add(have); if (!tsa(tok, token_semi)) return false; } else if (have == token_remove) { have = tok.next(); con->set_remove(have); if (!tsa(tok, token_semi)) return false; } else if (have == token_file) { if (!parse_syslogconfig(tok, dc, *con)) return false; } else { tok.token_error("threshold/ignore/add_command/remove_command/file", have); return false; } } if (!tsa(tok, token_semi)) { delete con; return false; } dc.add_context(con); return true; } //////////////////////////////////////////////// // parse a config file // bool load_conf(CONFIG &dc, const char *fn) { int count = 0; TOKEN tok(fn, &dc.config_files); while (true) { const char *have = tok.next(); if (!have) break; if (have == token_context) { if (!parse_context(tok, dc, NULL)) { tok.token_error("load_conf() failed to parse context"); return false; } else count++; } else { tok.token_error(token_context, have); return false; } } tok.token_error("load_conf() found %d contexts in %s", count, fn); return (!dc.contexts.empty()); } //////////////////////////////////////////////// // init the tokens // void token_init() { token_add = register_string("add_command"); token_bucket = register_string("bucket"); token_context = register_string("context"); 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_message = register_string("message"); 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"); }