////////////////////////////////////////////////////////////////////// // 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. // Split into modules // // 2021-06-18 // Wrapped in application object and added command line handling. // This includes: // - Getting config file name from CLI args // - Getting input and output filenams from CLI args // - Reading and writing from STDIN & STDOUT // - Send all non-data output to stderr ////////////////////////////////////////////////////////////////////// // TODO: map names according to time and requesting host. time is probably automatic #include #include #include #include #include #include #include #include "cli.h" #include "strutil.h" #include "data.h" #include "config.h" using namespace std; ////////////////////////////////////////////////////////////////////// // Application class to process files. ////////////////////////////////////////////////////////////////////// //#define DEBUG struct IPtraffic: public cBaseApp { Config config; StringList dns_ignore, dns_del; NameVal rdns; // Reverse DNS lookup cache istream *log; ostream *out; LiveBug bug; int line_no; IPtraffic(): out(&cout), log(0) { // I'd rather this initialization be static... dns_ignore.push_back("v=spf1"); dns_ignore.push_back("https:"); dns_del.push_back("NODATA-"); dns_del.push_back("NXDOMAIN-"); } ~IPtraffic() { if(log && log!=&cin) delete(log); } void dlog(const string msg) { #ifdef DEBUG cerr << line_no << ": " << msg << endl; #endif } // TODO: elaborate void help() { cerr << "\n" "iptraffic -c {config file} [-o {output file}] [{input file} [...]]\n"; ExitCode = 1; } unsigned do_switch(const char *sw) { if((sw[0]=='c' || sw[0]=='o') && sw[1]==0) return 1; throw CLIerror("Unrecognized Switch"); } void do_switch_arg(const char *sw, const std::string &val) { switch(*sw) { case 'c': config.load(val); if(!config.us.size()) throw CLIerror( "The configuration files MUST contain an [us] section with " "appropriate values" ); break; case 'o': if(out!=&cout) throw CLIerror("Output file has already been specified"); out = new ofstream(val.c_str()); // c_str(), really?!?! } } void do_arg(const char *fname) { if(log && log!=&cin) delete(log); log = 0; log = new ifstream(fname); ExitCode = do_log(); } // NOTE: the return values isn't really used yet but the channel is here if // it can be of use. int do_log() { if(!config.us.size()) throw CLIerror( "A configuration file must be specified before input files." ); Splits ln; int ict=0; NameVal::iterator nvp; string name, address, s; Conn conn; bool match; /// parse log file /// line_no=0; while((*log >> ln)) { line_no++; cerr << bug << ' ' << line_no << '\r' << flush; /// DNS query result /// // TODO: need to get more specific on tying us + them + time to DNS // TODO: doesn't seem that CNAMEs are getting attached to requests properly. // the logs are cryptic on this front. if(ln.count>8 && strncmp(ln.fields[4], "dnsmasq[", 8)==0) { if(ln[5]=="reply" || ln[5]=="cached") { 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); continue; } } /// process connections /// if((ln.count>5 // old style && ln[4]=="kernel:" && ln[5]=="ACCEPT" ) || (ln.count>6 // new style && ln[4]=="vmunix:" && ln[6]=="ACCEPT") ) { conn = ln; conn.compact(); if(!pre_match(config.us, conn.us)) conn.swap(); if((nvp=rdns.find(conn.them))!=rdns.end()) conn.name = nvp->second; if(config.ignores.find(conn)<0) *out << ln[0] << " " << ln[1] << " " << ln[2] << " " << conn << "\n"; else ict++; } } *out << flush; // make sure all data gets written. cerr << "\nIgnored: " << ict << endl; cerr << "Total rDNS: " << rdns.size() << "\n"; return 0; } int main() { try { cBaseApp::main(); if(!log) { // no inputs were specified run stdin. log = &cin; ExitCode = do_log(); } } catch(const CLIerror &e) { cerr << "ERROR: " << e.what() << "\n"; help(); } return ExitCode; } }; ////////////////////////////////////////////////////////////////////// // Run it ////////////////////////////////////////////////////////////////////// MAIN(IPtraffic)