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.
 
 
 
 

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