view src/syslogconfig.cpp @ 37:e4eb969dfc4a

shutdown removes iptables entries that we added
author carl
date Thu, 08 Nov 2007 11:07:05 -0800
parents 6a2f26976898
children 26c29da3fbdf
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>

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_message;
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, char *message);
    void leak(int amount, CONFIG &con);
    void free_all(CONFIG &con);
    void update(int ip, bool added, char *file_name, int pattern_index, char *message);
    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, 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, 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, NULL);
                changed(con, ip, false);
            }
            violations.erase(i++);
        }
        else {
            b.count -= amount;
            i++;
        }
    }
}


void IPR::free_all(CONFIG &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, char *file_name, int pattern_index, 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(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_, 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, 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, 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);
}


////////////////////////////////////////////////
//
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;
}


void CONFIG::free_all() {
    recorder.free_all(*this);
}

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;
    char *msg = NULL;
    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 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);
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_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");
}