////////////////////////////////////////////////////////////////////// // IP traffic analyzer // Written by Jonathan A. Foster // 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 // 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 #include #include #include #include #include #include #include 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) 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 &list, const string &s) { for( vector::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; xgtr.us) return 1; } // TODO: auto-wildcard port based on in? if(us_port && gtr.us_port) { // 0 = no comparison wildcard if(us_portgtr.us_port) return 1; } if(them!="*" && gtr.them!="*") { if(themgtr.them) return 1; } if(them_port && gtr.them_port) { // 0 = no comparison wildcard if(them_portgtr.them_port) return 1; } // TODO: do we want to consider the name? if(name!="*" && gtr.name!="*") { if(namegtr.name) return 1; } if(protocolgtr.protocol) return 1; if(ingtr.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 { int find(Conn &needle) { int r; for(r=0; r=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 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; }