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.
 
 
 
 

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