view src/syslogconfig.cpp @ 51:206448c00b55 stable-1-0-12

Allow multiple contexts with independent add/remove commands.
author Carl Byington <carl@five-ten-sg.com>
date Sat, 24 Jan 2009 15:52:20 -0800
parents ba0259c9e411
children b45dddebe8fc
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
const int maxlen = 1000;    // used for snprintf buffers


////////////////////////////////////////////////
//

IPR::IPR() {
    reference_count = 0;
}

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)) {
        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, message);
                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, message);
                    changed(con, ip, true);
                }
            }
        }
    }
}


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 <= amount) {
            if (b.latch) {
                update(ip, false, NULL, 0, NULL);
                changed(con, ip, false);
            }
            violations.erase(i++);
        }
        else {
            b.count -= amount;
            i++;
        }
    }
}


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.latch) {
            update(ip, false, NULL, 0, NULL);
            changed(con, ip, false);
        }
    }
    violations.clear();
}


void IPR::update(int ip, bool added, const char *file_name, int pattern_index, const char *message) {
    if (debug_syslog > 2) {
        char buf[maxlen];
        in_addr ad;
        ad.s_addr = htonl(ip);
        if (added) {
            if (message) snprintf(buf, maxlen, "dropping traffic from/to %s based on %s in %s", inet_ntoa(ad), message, file_name);
            else         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(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 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;
        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");
}