|
- //////////////////////////////////////////////////////////////////////
- // IP traffic analyzer
- // Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net>
- // Started April 23rd, 2021
- //
- // The idea is to analyze iptables LOG entries in combination with
- // DNSmasq's query log entries and combine them to list the hosts
- // that were accessed. The main reasons for not just inspecting HTTP
- // packets through a netfilter socket is due to HTTPS hiding the
- // "host" field. So I'm deducing based on DNS query timing.
- //
- // NOTE: its assumed that the log being processed is in chronological
- // order. This is the usual way things get logged. ;-)
- //
- // 2021-05-14 <ChipMaster@YeOlPiShack.net>
- // Dumbed down for C++ < 11.
- //////////////////////////////////////////////////////////////////////
-
- //////////////////////////////////////////////////////////////////////
- // Additional Router setup:
- //
- // ipset -N evilhosts iphash
- // ipset -N evilnets nethash
- //////////////////////////////////////////////////////////////////////
-
- //////////////////////////////////////////////////////////////////////
- // Obvious ignores:
- //
- // 10.10.10.1 -> 134.215.160.1 ICMP[8]
- //
- //////////////////////////////////////////////////////////////////////
- // TODO: map names according to time and host. time is probably automatic
-
- #include <string.h>
- #include <stdlib.h>
- #include <string>
- #include <iostream>
- #include <fstream>
- #include <stdexcept>
- #include <vector>
- #include <map>
- using namespace std;
-
-
-
- //////////////////////////////////////////////////////////////////////
- // Splits: a util class to devide a line into space sep pieces
- //////////////////////////////////////////////////////////////////////
- // TODO: implement begin() + end() to make "for( : )" work
- // TODO: implement field enclosing & escaping chars
-
- struct Splits {
-
- /// CONFIG ///
-
- enum { FieldMax=256, LineMax=1024 };
-
- /// properties ///
-
- char line[LineMax]; // Line buffer
- int len; // Length of line (after split())
- char sep; // Separator character.
- bool combine; // Treat multiple consecutive seps as one (combine)
- char *fields[FieldMax]; // pointers to fields in line
- int count; // How many fields there were
-
- // construct
- Splits(): count(0), len(0), sep(' '), combine(true) { line[LineMax-1] = 0; }
-
- // Convert field[] to string
- inline string operator[](int i) const { string s(fields[i]); return s; }
-
- // split line. Returns count.
- int split() {
- len = count = 0;
- if(!*line) return count;
- fields[0] = line;
- while(len<LineMax && line[len]) {
- if(line[len]==sep) {
- line[len++]=0;
- if(combine) while(len<LineMax && line[len]==sep) len++;
- if(++count<FieldMax) {
- // this shouldn't happen
- if(len>=LineMax) throw
- runtime_error("Splits::split: end of buffer null missing!");
- fields[count] = line+len;
- } else
- throw runtime_error("Splits::split: Too many fields in the line");
- } else
- len++;
- }
- return count++;
- }
- };
-
- // istream >> operator: getline() + .split()
- istream &operator>>(istream &in, Splits &sp) {
- if(in.getline(sp.line, sp.LineMax-1)) sp.split();
- return in;
- }
-
-
-
- //////////////////////////////////////////////////////////////////////
- // TSV version of Splits
- //////////////////////////////////////////////////////////////////////
-
- struct TSV: public Splits {
- TSV() { sep='\t'; combine=false; }
- };
-
-
-
-
- //////////////////////////////////////////////////////////////////////
- // Function to match a list of prefixes against a string
- //
- // Since C++ < 11 doesn't support constant vector initialization we'll
- // do this the old fashioned way with a null terminated char*[].
- //////////////////////////////////////////////////////////////////////
-
- bool pre_match(char **list, const string &s) {
- const char *p = s.c_str();
- for(; *list; list++)
- if(!strncmp(*list, p, strlen(*list))) return true;
- return false;
- }
- /* And because I think this may be useful in the future I'll hang on to it.
- bool pre_match(const vector<string> &list, const string &s) {
- for(
- vector<string>::const_iterator p=list.begin();
- p!=list.end();
- p++
- )
- if(s.substr(0, p->size())==*p) return true;
- return false;
- }*/
-
-
- //////////////////////////////////////////////////////////////////////
- // Connection between "us" and "them"
- //////////////////////////////////////////////////////////////////////
- typedef unsigned short word;
- struct Conn {
- string us; // address on our side
- word us_port; // the port on our side
- string them; // address on their side
- word them_port; // the port on their side
- string name; // name of the address
- string protocol; // protocol used to communicate
- bool in; // whether this was an inward bound connection.
-
- Conn(): in(false) {}
- Conn &clear() { us = them = name = protocol = ""; in=false; us_port = them_port = 0; }
-
- // swap polarity of record
- Conn &swap() {
- string s;
- int x;
- s = us;
- us = them;
- them =s;
- x = us_port;
- us_port = them_port;
- them_port = x;
- in=!in;
- return *this;
- }
-
- // scan & copy data from log record in
- Conn &operator=(const Splits &sp) {
- int x;
- clear();
- for(x=0; x<sp.count; x++) {
- if(!strncmp(sp.fields[x], "SRC=", 4)) {
- us = sp.fields[x]+4;
- continue;
- }
- if(!strncmp(sp.fields[x], "DST=", 4)) {
- them = sp.fields[x]+4;
- continue;
- }
- if(!strncmp(sp.fields[x], "SPT=", 4)) {
- us_port = atoi(sp.fields[x]+4);
- continue;
- }
- if(!strncmp(sp.fields[x], "DPT=", 4)) {
- them_port = atoi(sp.fields[x]+4);
- continue;
- }
- if(!strncmp(sp.fields[x], "TYPE=", 5) && protocol=="ICMP") {
- us_port = them_port = atoi(sp.fields[x]+5);
- continue;
- }
- if(!strncmp(sp.fields[x], "PROTO=", 6))
- protocol = sp.fields[x]+6;
- }
- }
-
- // TODO: does < > have any actual meaning in this context?
- int cmp(const Conn >r) const {
- if(us!="*" && gtr.us!="*") {
- if(us<gtr.us) return -1;
- if(us>gtr.us) return 1;
- }
- // TODO: auto-wildcard port based on in?
- if(us_port && gtr.us_port) { // 0 = no comparison wildcard
- if(us_port<gtr.us_port) return -1;
- if(us_port>gtr.us_port) return 1;
- }
- if(them!="*" && gtr.them!="*") {
- if(them<gtr.them) return -1;
- if(them>gtr.them) return 1;
- }
- if(them_port && gtr.them_port) { // 0 = no comparison wildcard
- if(them_port<gtr.them_port) return -1;
- if(them_port>gtr.them_port) return 1;
- }
- // TODO: do we want to consider the name?
- if(name!="*" && gtr.name!="*") {
- if(name<gtr.name) return -1;
- if(name>gtr.name) return 1;
- }
- if(protocol<gtr.protocol) return -1;
- if(protocol>gtr.protocol) return 1;
- if(in<gtr.in) return -1;
- if(in>gtr.in) return 1;
- return 0;
- }
-
- inline bool operator<(const Conn >r) const { return cmp(gtr) <0; }
- inline bool operator<=(const Conn >r) const { return cmp(gtr)<=0; }
- inline bool operator>(const Conn >r) const { return cmp(gtr) >0; }
- inline bool operator>=(const Conn >r) const { return cmp(gtr)>=0; }
- inline bool operator==(const Conn >r) const { return cmp(gtr)==0; }
- inline bool operator!=(const Conn >r) const { return cmp(gtr)!=0; }
-
- };
-
- // A text output of this record
- ostream &operator<<(ostream &out, const Conn &c) {
- out << c.us
- << ( c.in ? " <- " : " -> " )
- << c.them
- << " " << c.protocol
- << "[" << ( c.in ? c.us_port : c.them_port ) << "] "
- << c.name;
- return out;
- }
-
- // Copy data from TSV in
- const TSV &operator>>(const TSV &tsv, Conn &conn) {
- if(tsv.count<7) throw runtime_error("Conn=TSV: too few columns");
- conn.clear();
- conn.us = tsv[0];
- conn.us_port = atoi(tsv.fields[1]);
- conn.them = tsv[2];
- conn.them_port = atoi(tsv.fields[3]);
- conn.name = tsv[4];
- conn.protocol = tsv[5];
- conn.in = tsv[6]=="1";
- return tsv;
- }
-
-
-
- //////////////////////////////////////////////////////////////////////
- // List of connections
- //////////////////////////////////////////////////////////////////////
-
- struct ConnList: public vector<Conn> {
- int find(Conn &needle) {
- int r;
- for(r=0; r<size(); r++) if((*this)[r]==needle) return r;
- return -1;
- }
- };
-
-
-
- //////////////////////////////////////////////////////////////////////
- // Busy indicator aka. "Live Bug"
- //////////////////////////////////////////////////////////////////////
-
- struct LiveBug {
- string seq;
- char pre;
- int p;
- LiveBug(): seq("-\\|/"), pre('\r'), p(0) {}
- inline char next() { if(p>=seq.size()) p=0; return seq[p++]; }
- };
- ostream &operator<<(ostream &o, LiveBug &bug) {
- return o << bug.pre << bug.next();
- }
-
-
-
- //////////////////////////////////////////////////////////////////////
- // Roll through file
- //////////////////////////////////////////////////////////////////////
- //#define DEBUG
-
- typedef map<string,string> NameVal;
-
- // TODO: define us[] in conf file
- char *us[] = { (char *)"10.10.10.", (char *)"192.168.255.", (char *)"2001:470:a:169:", 0 };
- char *dns_ignore[] = { (char *)"v=spf1", (char *)"https:", 0 };
- char *dns_del[] = { (char *)"NODATA-", (char *)"NXDOMAIN-", 0 };
- #define PATH "/srv/backups/iptraffic"
- ifstream log(PATH "/test.log");
- ofstream out(PATH "/processed.log");
- Splits ln;
- int lnno = 0, ict = 0;
- LiveBug bug;
- NameVal rdns;
- NameVal::iterator nvp;
- string name, address, s;
- Conn conn;
- bool match;
- ConnList ignores;
-
-
-
- void dlog(const string msg) {
- #ifdef DEBUG
- cerr << "\r" << lnno << ": " << msg << endl;
- #endif
- }
-
-
-
- int main(int argc, char **argv) {
-
- /// Load lists ///
-
-
-
- /// Read in ignore list ///
-
- {
- TSV tsv;
- ifstream in(PATH "/ignores.lst");
- while(in >> tsv) {
- if(tsv.count>6) {
- tsv >> conn;
- ignores.push_back(conn);
- }
- }
- }
-
- /// parse log file ///
-
- while((log >> ln)) {
- lnno++;
- cout << bug << " " << lnno << flush;
-
- /// DNS query result ///
-
- // TODO: need to get more specific on tying us + them + time to DNS
- if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) {
- if(ln[5]=="reply") {
- name = ln[6];
- address = ln[8];
- // Hmm... is this reply an address?
- if(pre_match(dns_ignore, address)) continue; // nope
- if(pre_match(dns_del, address)) continue; // does not exist reply
- if((nvp=rdns.find(address))!=rdns.end()) {
- if(nvp->second==name) continue;
- dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name);
- }
- rdns[address] = name;
- dlog("Added "+address+" = "+name);
- #ifdef DEBUG
- cout << '\r' << lnno << ": " << name << endl;
- #endif
- continue;
- }
- }
-
- /// process connections ///
-
- if(ln.count>5
- && ln[4]=="kernel:"
- && ln[5]=="ACCEPT"
- ) {
- conn = ln;
- if(!pre_match(us, conn.us)) conn.swap();
- if((nvp=rdns.find(conn.them))!=rdns.end())
- conn.name = nvp->second;
- if(ignores.find(conn)<0)
- out << ln[0] << " " << ln[1] << " " << ln[2] << " " << conn << "\n";
- else
- ict++;
- }
- }
- cout << "\nIgnored: " << ict << endl;
- cout << "Total rDNS: " << rdns.size() << "\n";
- return 0;
- }
|