The Poor Man's (or Woman's) Intrusion Detection System
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

318 lines
9.7 KiB

  1. //////////////////////////////////////////////////////////////////////
  2. // IP traffic monitor
  3. // Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net>
  4. // Started August 11th, 2021
  5. //
  6. // The idea is to analyze iptables LOG entries in combination with
  7. // DNSmasq's query log entries and combine them to list the hosts
  8. // that were accessed. This will be read in real time from SysLogD
  9. // using the "pipe" feature. Access records will be logged to MySQL
  10. // for reporting and record keeping.
  11. //////////////////////////////////////////////////////////////////////
  12. // TODO: catch signals and attempt removal of PID file during shutdown.
  13. #include <string.h>
  14. #include <errno.h>
  15. #include <sys/types.h>
  16. #include <sys/stat.h>
  17. #include <syslog.h>
  18. #include <unistd.h>
  19. #include <string>
  20. #include <iostream>
  21. #include <fstream>
  22. #include <stdexcept>
  23. #include <vector>
  24. #include <map>
  25. #include "appbase.h"
  26. using namespace std;
  27. //////////////////////////////////////////////////////////////////////
  28. // Application class to store data passed in through a pipe or
  29. // file(s).
  30. //////////////////////////////////////////////////////////////////////
  31. //#define DEBUG
  32. struct TrafficMon: public TrafficMonBaseApp {
  33. LogAnalyzer analyze;
  34. istream *log;
  35. LiveBug bug;
  36. bool setup_db;
  37. bool piping; // are we sucking on a pipe?
  38. bool background_me; // daemonize?
  39. string pid_file; // Name of file to write PID in
  40. int inp_ct; // How many inputs were on the CLI
  41. bool running; // Whether or not its time to run logs
  42. long long line_no; // current line # in the active input
  43. TrafficMon():
  44. log( 0),
  45. setup_db( false),
  46. piping( false),
  47. background_me(false),
  48. inp_ct( 0),
  49. running( false),
  50. line_no( 0)
  51. {
  52. config = new MonitorBaseConf;
  53. analyze.us = &config->us.vals;
  54. }
  55. int help() {
  56. cerr <<
  57. "\n"
  58. "trafficmon -c {config file} [-i] [-d] [-b] [-p {pipe name} | [{log name} ...]]\n"
  59. "\n"
  60. "Arguments:\n"
  61. " -c Load configuration from file (required)\n"
  62. " -i PID file to write (optional, use with -b)\n"
  63. " -d Create data tables (optional, use once)\n"
  64. " -b Run in background - daemonize (optional, use -i too)\n"
  65. " -p read from pipe creating it if needed (optional, good with -b)\n"
  66. "\n"
  67. "Other arguments are log files to be processed. This can be used to bulk\n"
  68. "load data prior to going live with an always on daemon or for catching\n"
  69. "up if the daemon stopped for some reason.";
  70. return ExitCode = 1;
  71. }
  72. unsigned do_switch(const char *sw) {
  73. if(sw[1]==0) {
  74. switch(*sw) {
  75. case 'b': background_me = true; return 0;
  76. case 'd': setup_db = true; return 0;
  77. case 'i': return 1;
  78. case 'p': piping = true; return !running; // second pass treats it as a file
  79. }
  80. }
  81. return TrafficMonBaseApp::do_switch(sw);
  82. }
  83. void do_switch_arg(const char *sw, const string &val) {
  84. // If "running" is set then we've already done this. So skip it!
  85. if(running) return;
  86. if(sw[1]==0) switch(*sw) {
  87. /// PID file to write ID in if we're daemonizing ///
  88. case 'i':
  89. pid_file = val;
  90. return;
  91. /// Make pipe if requested ///
  92. case 'p':
  93. // This can't wait for "running" since we want it to fail in foreground
  94. // Make read/write by process owner (root) only
  95. if(mkfifo(val.c_str(), 0600)<0 && errno!=17 /*already exists*/) {
  96. ExitCode = 2;
  97. throw runtime_error("Making pipe raised error "+str(errno));
  98. }
  99. inp_ct++;
  100. }
  101. TrafficMonBaseApp::do_switch_arg(sw, val);
  102. }
  103. void do_arg(const char *fname) {
  104. // if not "running" we don't want to do this yet. Just count inputs.
  105. if(!running) {
  106. inp_ct++;
  107. return;
  108. }
  109. // the only thing we expect on the CLI is a log/pipe name
  110. if(log==&cin) log = 0; // won't happen right now
  111. if(log)
  112. ((ifstream*)log)->close();
  113. else
  114. log = new ifstream;
  115. if(!background_me) cerr << fname << ":\n";
  116. restart:
  117. ((ifstream*)log)->open(fname);
  118. ExitCode = run();
  119. if(piping) {
  120. // If a process closes the write end of the pipe, like during log
  121. // rotation, we receive an EOF, end up here, and have to re-open the pipe
  122. // to be able to receive more on it.
  123. ((ifstream*)log)->close();
  124. sleep(1); // This is just to make sure we don't hog CPU in a loop
  125. goto restart;
  126. }
  127. }
  128. // This is the actual data process
  129. // probably should daemonize around here.
  130. int run() {
  131. string l;
  132. // prepare insert statement
  133. cppdb::statement db_add = db <<
  134. "INSERT INTO connections "
  135. "(us, us_port, them, them_port, them_name, protocol, inbound) "
  136. "VALUES (?,?,?,?,?,?,?)";
  137. /// parse log file ///
  138. line_no=0;
  139. while(getline(*log, l)) {
  140. line_no++;
  141. if(!background_me)
  142. cerr << bug << ' ' << line_no << '\r' << flush;
  143. /// process connections ///
  144. if(analyze.line(l)) {
  145. // TODO: time stamp handling?
  146. // insert record
  147. db_add.reset();
  148. db_add << analyze.conn.us << analyze.conn.us_port << analyze.conn.them
  149. << analyze.conn.them_port << analyze.conn.name
  150. << analyze.conn.protocol << analyze.conn.in
  151. << cppdb::exec;
  152. }
  153. }
  154. // In a real pipe situation this should never get reached.
  155. if(!background_me) {
  156. cerr << "\nLines: " << line_no
  157. << "\nTotal rDNS: " << analyze.rdns.size() << '\n' << endl;
  158. }
  159. return 0;
  160. }
  161. void create_tables() {
  162. /// Connection recrods ///
  163. db << "CREATE TABLE connections ("
  164. "id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,"
  165. "tstamp TIMESTAMP NOT NULL," // default is NOW()
  166. "us CHAR(80) NOT NULL DEFAULT '',"
  167. "us_port INT NOT NULL DEFAULT 0,"
  168. "them CHAR(80) NOT NULL DEFAULT '',"
  169. "them_port INT NOT NULL DEFAULT 0,"
  170. "them_name CHAR(128) NOT NULL DEFAULT '',"
  171. "protocol CHAR(12) NOT NULL DEFAULT '',"
  172. "inbound TINYINT(1) NOT NULL DEFAULT 0,"
  173. "INDEX tstamp(tstamp, them_name)"
  174. ") Engine=MyISAM"
  175. << cppdb::exec;
  176. // NOTE: MyISAM is thousands of times faster than InnoDB for this sort
  177. // of work load. Mostly continuous writes with occasional reads.
  178. // But will it work for all I'm going to do? AriaDB is slower but
  179. // significantly faster than InnoDB.
  180. /// Status of domain names ///
  181. db << "CREATE TABLE dns ("
  182. "name CHAR(128) NOT NULL PRIMARY KEY,"
  183. "decided TIMESTAMP NOT NULL,"
  184. "status TINYINT(1) NOT NULL DEFAULT 0,"
  185. "note CHAR(128) NOT NULL DEFAULT ''"
  186. ") Engine=MyISAM"
  187. << cppdb::exec;
  188. /// wild card DNS list ///
  189. // NOTE: All of these names are treated as if prefixed with "*.". At this
  190. // time there are only plans to implement this as a black list.
  191. // Many domains are sevices with threat potential and discovering
  192. // all of the host and subdomains may not be viable. Think ad and
  193. // tracking services.
  194. db << "CREATE TABLE dns_wild ("
  195. "name CHAR(128) NOT NULL PRIMARY KEY,"
  196. "decided TIMESTAMP NOT NULL,"
  197. "status TINYINT(1) NOT NULL DEFAULT 2,"
  198. "note CHAR(128) NOT NULL DEFAULT ''"
  199. ") Engine=MyISAM"
  200. << cppdb::exec;
  201. }
  202. int main() {
  203. int x;
  204. try {
  205. if(x=TrafficMonBaseApp::main()) return x;
  206. if(!config->us.vals.size()) throw CLIerror(
  207. "The configuration files MUST contain an [us] section with "
  208. "appropriate values"
  209. );
  210. if(!config->traffic_mon.vals.size()) throw CLIerror(
  211. "The configuration files MUST contain an [Traffic Mon] section with "
  212. "appropriate values"
  213. );
  214. if(piping && inp_ct!=1) throw CLIerror(
  215. "Pipe requires one and only one file name to read from."
  216. );
  217. if(setup_db) create_tables();
  218. // if requested attempt daemonizing.
  219. if(background_me) {
  220. if(inp_ct==0) throw CLIerror(
  221. "Backgrounding requires the specification of an input source. "
  222. "STDIN is unavailable when we move to background."
  223. );
  224. if(daemon(0,0)<0) throw CLIerror("Failed to switch to background");
  225. // If we get here we're in the background. No STDIN, STDOUT, STDERR
  226. if(pid_file!="") { // pid_file requested
  227. ofstream pf(pid_file.c_str());
  228. pf << getpid();
  229. }
  230. }
  231. } catch(const CLIerror &e) {
  232. cerr << "ERROR: " << e.what() << "\n";
  233. help();
  234. return ExitCode ? ExitCode : 1; // JIC someone sets a different exit code
  235. }
  236. try {
  237. // CLI processed now lets analyze data.
  238. running = true;
  239. // TODO: this is going to break things? Probably should prevent reloading conf. DB is opened in TrafficMonBaseApp::main()
  240. // Actually I think it may actualy work...
  241. cBaseApp::main(); // re-run CLI args for inputs
  242. if(!log) {
  243. // no inputs spec'd on CLI assume stdin (we're not a daemon)
  244. log = &cin;
  245. ExitCode = run();
  246. }
  247. } catch(const exception &e) {
  248. if(ExitCode==0) ExitCode = 2;
  249. if(background_me) {
  250. openlog("trafficmon", LOG_CONS | LOG_PID, LOG_DAEMON);
  251. syslog(LOG_ALERT, "%s", e.what());
  252. closelog();
  253. } else
  254. cerr << "ERROR: " << e.what() << "\n";
  255. }
  256. return ExitCode;
  257. }
  258. };
  259. //////////////////////////////////////////////////////////////////////
  260. // Run it
  261. //////////////////////////////////////////////////////////////////////
  262. MAIN(TrafficMon)