Author | SHA1 | Message | Date |
---|---|---|---|
Jon Foster | ddca2693c4 |
Adapt CNAME handler to handle more CNAMEs
A CNAME record can point to another name that is a CNAME, and so on. This prepares the CNAME resolver to deal with this. It now properly records clockworkpi.com, which is a CNAME -> WIX -> Fastly. |
2 years ago |
Jon Foster | d12a3e9f4d |
Traffic monitor fixes and block exports
* *NEW* dnsblacklist to make "hosts" style block lists * *NEW* domblacklist to make dnsmasq resolve entries to block entire domains regardless of host or subdomain. * Several fixes * Refactor CLI apps for more code re-use. * More helpful error messages. * Add whole (aka wildcard) domain block handling. |
2 years ago |
Jon Foster | bb0278f2a5 |
Control Panel fixes & whole domain blocks
* Fix config files and init script * Help text and error coloring in WUI template. * EOL white space. :-/ * Add whole domain blocking |
2 years ago |
Jon Foster | 9ca68a65f5 |
CNAME handling
* Improve CNAME reporting to use original (alias) names. This seems the most normal and intuitive. * CLI interface improvements (cli.*): - help message - gripe about invalid switches. - return exit code 1 in iptraffic for invalid configuration. * Expand Makefile * Update DPAK |
2 years ago |
Jon Foster | 2d57afcacd | Minor package improvments | 3 years ago |
Jon Foster | ffab54d30d |
controlpanel: URL handling & debugging
Improved group handling in init.d script too. |
3 years ago |
Jon Foster | cd7d62ca05 |
Begin realtime tracking
* Program to run as daemon and listen to SysLogD on a pipe. * C++CMS web app to show accumulated domain names and set their status * Sample configs to be used to set things up |
3 years ago |
Jon Foster | 2876b4125a |
Import MiniINI and refactor config.o
In preparation for more tools with differing config data needs I pulled in my "Mini INI" crackers and some additional string utilities. The Config class has been refactored to use them and the individual group parsers refactored onto appropriate MiniINIgroup based classes. |
3 years ago |
Jon Foster | 8a33f5fdd8 |
Break analyzer out of iptraffic & tests
Added tests for testing address wild card matching and a test jig to put tests in. I moved the core of the log analyzer out of iptraffic and into data.o for use in multiple tools. |
3 years ago |
@@ -3,3 +3,14 @@ | |||
/iptraffic | |||
/log | |||
/README.html | |||
/trafficmon/badtrafficrpt | |||
/trafficmon/trafficmon | |||
/controlpanel/trafficctrl | |||
# I use these as output of C++ generators. Don't store them: | |||
*.cxx | |||
# Debian (DPAK) packaging artifacts | |||
*.deb | |||
/tmp-dpak-poorman-ids/ | |||
@@ -1,28 +1,60 @@ | |||
iptraffic: iptraffic.cpp strutil.o data.o config.o cli.o | |||
g++ -o $@ $@.cpp strutil.o data.o config.o cli.o | |||
# cm-20220225 testing controlpanel messages with symbols left in | |||
#O=-s | |||
config.o: config.cpp config.h strutil.o data.o | |||
g++ -c -o $@ config.cpp | |||
data.o: data.cpp data.h strutil.o | |||
g++ -c -o $@ data.cpp | |||
### Program Targets ### | |||
.PHONY: all controlpanel/trafficctrl | |||
all: iptraffic trafficmon/badtrafficrpt trafficmon/dnsblacklist trafficmon/domblacklist trafficmon/trafficmon controlpanel/trafficctrl | |||
controlpanel/trafficctrl: | |||
cd controlpanel && make trafficctrl | |||
iptraffic: iptraffic.cpp strutil.o data.o config.o cli.o miniini.o | |||
g++ $O -o $@ $@.cpp strutil.o data.o config.o cli.o miniini.o | |||
trafficmon/badtrafficrpt: trafficmon/badtrafficrpt.cpp cli.o miniini.o strutil.o trafficmon/appbase.o | |||
g++ $O -o $@ $@.cpp strutil.o cli.o miniini.o trafficmon/appbase.o -lcppdb | |||
trafficmon/dnsblacklist: trafficmon/dnsblacklist.cpp cli.o miniini.o strutil.o trafficmon/appbase.o | |||
g++ $O -o $@ $@.cpp strutil.o cli.o miniini.o trafficmon/appbase.o -lcppdb | |||
trafficmon/domblacklist: trafficmon/domblacklist.cpp cli.o miniini.o strutil.o trafficmon/appbase.o | |||
g++ $O -o $@ $@.cpp strutil.o cli.o miniini.o trafficmon/appbase.o -lcppdb | |||
trafficmon/trafficmon: trafficmon/trafficmon.cpp strutil.o data.o config.o cli.o miniini.o trafficmon/appbase.o | |||
g++ $O -o $@ $@.cpp strutil.o data.o config.o cli.o miniini.o trafficmon/appbase.o -lcppdb | |||
### Libs ### | |||
cli.o: cli.cpp cli.h | |||
g++ -c -o $@ cli.cpp | |||
g++ $O -c -o $@ cli.cpp | |||
strutil.o: strutil.cpp strutil.h | |||
g++ -c -o $@ strutil.cpp | |||
config.o: config.cpp config.h strutil.o data.o miniini.o | |||
g++ $O -c -o $@ config.cpp | |||
data.o: data.cpp data.h strutil.o | |||
g++ $O -c -o $@ data.cpp | |||
miniini.o: miniini.cpp miniini.h strutil.o | |||
g++ $O -c -o $@ miniini.cpp | |||
strutil.o: strutil.cpp strutil.h | |||
g++ $O -c -o $@ strutil.cpp | |||
trafficmon/appbase.o: trafficmon/appbase.cpp trafficmon/appbase.h cli.o miniini.o | |||
g++ $O -c -o $@ trafficmon/appbase.cpp | |||
.PHONY: run | |||
run: iptraffic | |||
./iptraffic | |||
### Source Maintenance ### | |||
.PHONY: clean distclean | |||
clean: | |||
rm *.o || true | |||
rm *.o */*.o || true | |||
distclean: clean | |||
rm iptraffic || true | |||
rm iptraffic trafficmon/trafficmon trafficmon/badtrafficrpt trafficmon/dnsblacklist trafficmon/domblacklist || true | |||
rm *.deb || true | |||
cd controlpanel && make distclean |
@@ -0,0 +1,17 @@ | |||
BUGS | |||
==== | |||
- The wild card blocks don't seem to be automatically moving DNS | |||
entries into the block list. | |||
IDEAS | |||
===== | |||
- what about drilling down on domains: domain.tld, then expand up | |||
levels if there are more than a couple of entries. | |||
- Some way to browse by workstation traffic | |||
- See hosts that accessed a domain in the lists. |
@@ -26,6 +26,12 @@ cBaseApp &cBaseApp::init(int argc, char **argv) { | |||
unsigned cBaseApp::do_switch(const char *arg) { | |||
throw CLIerror("Invalid switch '"+std::string(arg)+"'"); | |||
} | |||
int cBaseApp::main() { | |||
int i, ct; | |||
char *p; | |||
@@ -68,6 +74,15 @@ int cBaseApp::main() { | |||
int cBaseApp::help() { | |||
std::cerr << | |||
"Invalid command line arguments and the developer didn't provide any help." | |||
<< std::endl; | |||
return ExitCode = 1; | |||
} | |||
int cBaseApp::crash(const std::exception &e) { | |||
std::cerr << "Application crashed: " << e.what() << std::endl; | |||
return 216; // just a weird number hopefully not conflicting with anything else. | |||
@@ -93,7 +93,7 @@ struct cBaseApp { | |||
// | |||
// how many args needed for val | |||
virtual unsigned do_switch(const char *arg) { return 0; } | |||
virtual unsigned do_switch(const char *arg); | |||
// proccess a val for switch | |||
virtual void do_switch_arg(const char *sw, const std::string &val) { } | |||
// process a non-switch arg. | |||
@@ -127,6 +127,16 @@ struct cBaseApp { | |||
virtual int main(); | |||
/// Provide help text for CLI arg parse errors | |||
// | |||
// This is intended to show a command line help message on the terminal | |||
// about what the proper CLI syntax is. The return is the desired exit | |||
// code. The default is 1. This implementation will provide the app meta | |||
// data, if present. This simplified method is used so an exception is | |||
// not required to call it. | |||
virtual int help(); | |||
/// Catch exceptions /// | |||
// | |||
// This is called by the boiler plate main() (see bottom) when an excpetion | |||
@@ -6,58 +6,57 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
#include <fstream> | |||
#include <iostream> | |||
#include <stdexcept> | |||
#include "config.h" | |||
#include "strutil.h" | |||
void Config::load(const std::string &fname) { | |||
std::string l; | |||
std::ifstream f(fname.c_str()); | |||
TSV tsv; | |||
Conn conn; | |||
int ln=0; | |||
////////////////////////////////////////////////////////////////////// | |||
// INIusList | |||
////////////////////////////////////////////////////////////////////// | |||
void INIusList::add(const std::string &in) { | |||
std::string s=trim(in); | |||
if(s!="" && s[0]!='#') MiniINIlines::add(s); | |||
} | |||
while(std::getline(f, l)) { | |||
ln++; | |||
l = strip(l); | |||
if(l=="" || l[0]=='#') continue; | |||
if(l.size()>2 && l[0]=='[' && l.end()[-1]==']') { | |||
heading: | |||
if(l=="[us]") { | |||
////////////////////////////////////////////////////////////////////// | |||
// INIconnList | |||
////////////////////////////////////////////////////////////////////// | |||
void INIconnList::add(const std::string &in) { | |||
int i; | |||
if(in=="" || in[0]=='#') return; // remarks | |||
// TODO: we don't want to keep create+destroy-ing these? | |||
TSV tsv(in); | |||
if(tsv.count!=7) throw | |||
// TODO: really need a line number! | |||
std::runtime_error("INIconnList::add: Incorrect column count in config file line"); | |||
if(tsv.count>6) { | |||
i = vals.size(); | |||
vals.resize(i+1); | |||
tsv >> vals[i]; | |||
} | |||
} | |||
/// Read in "us" list /// | |||
while(std::getline(f, l)) { | |||
ln++; | |||
l=strip(l); | |||
if(l=="" || l[0]=='#') continue; | |||
if(l.size()>2 && l[0]=='[' && l.end()[-1]==']') goto heading; | |||
us.push_back(l); | |||
} | |||
// NOP right now, since I don't intend to be writing the config file. | |||
std::ostream &INIconnList::save(std::ostream &out) const { | |||
throw std::runtime_error("INIconnList::save: not implented."); | |||
} | |||
} else if(l=="[ignores]") { | |||
/// Read in ignore list /// | |||
while(std::getline(f, l)) { | |||
ln++; | |||
if(l=="" || l[0]=='#') continue; | |||
if(l.size()>2 && l[0]=='[' && l.end()[-1]==']') goto heading; | |||
tsv = l; | |||
if(tsv.count!=7) { | |||
std::cerr << "Incorrrect column count in config file line " << ln << std::endl; | |||
continue; | |||
} | |||
if(tsv.count>6) { | |||
tsv >> conn; | |||
ignores.push_back(conn); | |||
} | |||
} | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// Config | |||
////////////////////////////////////////////////////////////////////// | |||
} | |||
} | |||
Config::Config() { | |||
groups["us" ] = &us; | |||
groups["ignores"] = &ignores; | |||
} |
@@ -12,16 +12,49 @@ | |||
#ifndef __JFP_IPTRAFFIC_CONF_H__ | |||
#define __JFP_IPTRAFFIC_CONF_H__ | |||
#include <string> | |||
#include <vector> | |||
#include <ostream> | |||
#include "data.h" | |||
#include "miniini.h" | |||
#include "strutil.h" | |||
struct Config { | |||
std::vector<std::string> us; | |||
ConnList ignores; | |||
////////////////////////////////////////////////////////////////////// | |||
// INI group parser for "us" records | |||
// | |||
// This is mostly "raw lines" but we need to throw out remarks and WS. | |||
////////////////////////////////////////////////////////////////////// | |||
struct INIusList: public MiniINIlines { | |||
void add(const std::string &in); | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// INI group parser for "ignore" records | |||
////////////////////////////////////////////////////////////////////// | |||
void load(const std::string &fname); | |||
struct INIconnList: public MiniINIgroup { | |||
ConnList vals; | |||
// Read records | |||
void add(const std::string &in); | |||
// Write records | |||
std::ostream &save(std::ostream &out) const; | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// INI based Configuration container for IPtraffic | |||
////////////////////////////////////////////////////////////////////// | |||
struct Config: public MiniINI { | |||
INIusList us; | |||
INIconnList ignores; | |||
Config(); | |||
}; | |||
#endif |
@@ -0,0 +1,21 @@ | |||
# Optional compiler flags | |||
#O=-std=c++11 | |||
trafficctrl: trafficctrl.cpp data.h ../strutil.o mainskin.o | |||
g++ $O -o $@ $@.cpp mainskin.o ../strutil.o -lcppcms -lcppdb -lbooster | |||
../strutil.o: ../strutil.cpp ../strutil.h | |||
cd .. && make strutil.o | |||
mainskin.cxx: mainskin.tmpl | |||
cppcms_tmpl_cc -o $@ mainskin.tmpl | |||
mainskin.o: mainskin.cxx data.h | |||
g++ $O -c mainskin.cxx | |||
.PHONY: clean distclean | |||
clean: | |||
rm *.o *.cxx || true | |||
distclean: clean | |||
rm trafficctrl || true |
@@ -0,0 +1,12 @@ | |||
* Add comments to "dns" decision table | |||
* "dns.decided" field should default to the last hit in "connections". | |||
* format table centered with the title. | |||
* Extend DNS mechanism to incorporate anonymous (no DNS) connections. | |||
* Daemonization mechanism to allow us to run as non-root. | |||
* C++CMS creates socket as original user:group (typically root:root). Yet the | |||
PID file is written as the requested user:group. This means that permissions | |||
on the socket have to be handled outside of C++CMS or it needs some patchery. | |||
* Tool to read JSON conf and supply values to "init" script. | |||
* Some list filtering tools in the WUI to target actions on specific subsets | |||
of DNS names |
@@ -0,0 +1,37 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Control Panel and domain control data | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started August 13th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// Data for dealing with white / black lists. | |||
////////////////////////////////////////////////////////////////////// | |||
#ifndef __CONTROL_DATA_H__ | |||
#define __CONTROL_DATA_H__ | |||
#include <string> | |||
#include <vector> | |||
#include <cppcms/base_content.h> | |||
//#include <booster/function.h> | |||
//#include <cppcms/serialization.h> | |||
struct Domain { | |||
enum STATUS {undecided, accepted, blocked}; | |||
std::string name; // domain name | |||
std::string decided; // since C++ doesn't have a date/time | |||
int status; // how we should handle it | |||
}; | |||
struct DomainList :public cppcms::base_content { | |||
std::vector<Domain> list; | |||
std::string filter; // Which filter was used to show list | |||
std::string error; | |||
int page, pages, page_size, count; | |||
}; | |||
#endif |
@@ -0,0 +1,12 @@ | |||
# Configuration for init.d/trafficctrl. All entries remarked out below are the | |||
# defaults. | |||
# Configuration file for the TrafficCtrl server | |||
# NOTE: service won't start until this is set | |||
#CONF=/etc/poorman-ids/sample.js | |||
# Where "run" files are placed. This is the Debian+ default: | |||
#RUN=/run | |||
# This needs to match the "daemon.lock" entry in the $CONF file. | |||
#PID=$RUN/poorman-ids/trafficctrl.pid | |||
# The group to run as. This is the Debian default for web servers. | |||
GROUP=www-data |
@@ -0,0 +1,117 @@ | |||
#!/bin/sh | |||
### BEGIN INIT INFO | |||
# Provides: trafficctrl | |||
# Required-Start: $local_fs | |||
# Required-Stop: $local_fs | |||
# Default-Start: 2 3 4 5 | |||
# Default-Stop: 0 1 6 | |||
# Short-Description: Control categories of network traffic | |||
# Description: This service provides an HTTP or FastCGI service to | |||
# provide a control panel for classifying web traffic. | |||
# It does not take any action by itself. | |||
# | |||
# NOTE: you need to use trafficmon to setup the DB first. | |||
### END INIT INFO | |||
NAME="trafficctrl" | |||
DAEMON="/usr/sbin/$NAME" | |||
RUN=/run | |||
CONF="" | |||
PID="" | |||
GROUP="" | |||
# Pull in config | |||
if [ -r "/etc/default/$NAME" ]; then | |||
. /etc/default/$NAME | |||
fi | |||
### Setup control variables ### | |||
# This is where we put PID files and the pipe | |||
RUN="$RUN/poorman-ids" | |||
# NOTE: this needs to match what $CONF says | |||
[ -n "$PID" ] || PID="$RUN/$NAME.pid" | |||
mkdir -p "$RUN" | |||
[ -z "$GROUP" ] || GROUP="-g $GROUP" | |||
### ACTIONS ### | |||
# The main service command | |||
CTRL() { | |||
start-stop-daemon --pidfile "$PID" --exec "$DAEMON" "$@" | |||
} | |||
do_start() { | |||
echo -n "Starting Traffic Control: " | |||
if [ -z "$CONF" ]; then | |||
echo "NOT CONFIGURED" | |||
return 0 | |||
fi | |||
if CTRL --start --oknodo --umask 007 $GROUP -- -c "$CONF"; then | |||
echo "OK" | |||
return 0 #JIC | |||
else | |||
echo "FAIL" | |||
return 1 | |||
fi | |||
} | |||
do_stop() { | |||
echo -n "Stoping Traffic Control: " | |||
if CTRL --stop --remove-pidfile; then | |||
echo "OK" | |||
return 0 #JIC | |||
else | |||
echo "FAIL" | |||
return 1 | |||
fi | |||
} | |||
do_status() { | |||
echo -n "Traffic Control is: " | |||
if CTRL --status; then | |||
echo "Up" | |||
return 0 #JIC | |||
else | |||
echo "Down" | |||
return 1 | |||
fi | |||
} | |||
### Main() | |||
case "$1" in | |||
start) | |||
do_start | |||
;; | |||
stop) | |||
do_stop | |||
;; | |||
restart) | |||
do_status && do_stop | |||
do_start | |||
;; | |||
status) | |||
do_status | |||
;; | |||
*) | |||
echo "$0 {start | stop | restart | status}" | |||
;; | |||
esac |
@@ -0,0 +1,28 @@ | |||
# This is based on a Debian 10 install of LigHTTPd install. | |||
# Include the 10-fastcgi.conf or: | |||
# server.modules += ( "mod_fastcgi" ) | |||
#fastcgi.debug = 1 | |||
fastcgi.server = ( | |||
"/webmonitor" => | |||
( "trafficctrl" => | |||
( "socket" => "/run/poorman-ids/trafficctrl.fcgi", | |||
"check-local" => "disable", | |||
# "fix-root-scriptname" => "enable", | |||
#"docroot" => "/" # remote server may use | |||
# its own docroot | |||
) | |||
) | |||
) | |||
# Password restriction could be done something like this: | |||
auth.backend = "htpasswd" | |||
auth.backend.htpasswd.userfile = "/etc/lighttpd/lighttpd.users" | |||
auth.require += ( "/webmonitor" => ( | |||
"method" => "basic", | |||
"realm" => "Web Monitor", | |||
#"require" => "user=root" | |||
), ) | |||
# Check LigHTTPd docs for your version. |
@@ -0,0 +1,114 @@ | |||
<% c++ // Why isn't this done automatically?!?! %> | |||
<% c++ #include <cppcms/view.h> %> | |||
<% c++ #include "data.h" %> | |||
<% skin mainskin %> | |||
<% view domain_list uses ::DomainList %> | |||
<% template title() %><%= filter %> Domain List<% end %> | |||
<% template menu() %> | |||
<b>Filters: </b> | |||
<a href="<% url "" %>">To Be Decided</a> | | |||
<a href="<% url "accepted" %>">Accepted</a> | | |||
<a href="<% url "blocked" %>">Blocks</a> | |||
<% end template %> | |||
<% template pager(int no, int ct) %> | |||
<% if (ct>1) %><div class="pager"> | |||
<% if (no>1) %> | |||
<a href="?pg=1">|<</a> | |||
<a href="?pg=<% c++ out() << no-1; %>"><<</a> | |||
<% end if %> | |||
<%= no %> of <%= ct %> | |||
<% if (no<ct) %> | |||
<a href="?pg=<% c++ out() << no+1; %>">>></a> | |||
<a href="?pg=<%= ct %>">>|</a> | |||
<% end if %> | |||
</div><% end if %> | |||
<% end template %> | |||
<% template render() %> | |||
<html><head> | |||
<title><% include title() %></title> | |||
<style> | |||
h1 { text-align: center; } | |||
table { | |||
margin-top: 1em; | |||
border-bottom: 2px solid black; | |||
} | |||
table th { | |||
border-top: 2px solid black; | |||
border-bottom: 2px solid black; | |||
} | |||
div.menu { | |||
border-top: 2px solid black; | |||
border-bottom: 2px solid black; | |||
padding: 0.25em; | |||
} | |||
div.pager { | |||
font-weight: bold; | |||
} | |||
</style> | |||
</head><body> | |||
<div class="menu"><% include menu() %></div> | |||
<h1><% include title() %></h1> | |||
<div id="content"> | |||
<% foreach domain rowid r from 1 in list %> | |||
<% include pager(page, pages) %> | |||
<% if not empty error %> | |||
<p style="color: red"><i><b><%= error %></b></i></p> | |||
<% end %> | |||
<form method="POST"> | |||
Whole Domain: <input name="domain" size=50> | |||
<p><i><b>NOTE:</b> the root domain name listed here will match any records in | |||
this list with the same suffix and record the decision as chosen below. | |||
<% if ( content.filter == "undecided" ) %> | |||
Whole domains can be blocked by prefixing them with "*.". This means that | |||
even if the exact host name or subdomain is not listed here it will get | |||
blocked. | |||
<% end %> | |||
<table> | |||
<tr><th><select name="op" value="0"> | |||
<option value="0">Undecided</option> | |||
<option value="1">Accept</option> | |||
<option value="2">Block</option> | |||
</select> | |||
<input type="submit" value=">"> | |||
</th> | |||
<th>Domain</th> | |||
<th>When Decided</th> | |||
</tr> | |||
<% item %> | |||
<tr> | |||
<td> | |||
<span style="float: left"><% c++ out() << r+content.page_size*(content.page-1); %>.</span> | |||
<center><input type=checkbox name="id" value="<%= domain.name %>"></center> | |||
</td> | |||
<td><%= domain.name %></td> | |||
<td><%= domain.decided %></td> | |||
</tr> | |||
<% end item %> | |||
<tr><td colspan="3">of <%= count %></td></tr> | |||
</table></form> | |||
<% include pager(page, pages) %> | |||
<% empty %> | |||
<h2>Nothing available</h2> | |||
<% end %> | |||
</div> | |||
</body></html> | |||
<% end template %> | |||
<% end view %> | |||
<% end skin %> |
@@ -0,0 +1,68 @@ | |||
// This configuration file is used by C++CMS and trafficctrl. Most of the | |||
// format is described on the C++CMS website at: | |||
// http://cppcms.com/wikipp/en/page/cppcms_1x_config | |||
// Its syntax is JSON with the addition of C++ single line remarks, as seen | |||
// here. | |||
// NOTE: At this time no form of security is provided in this application. Its | |||
// expected it will be run through a proxy that will provide httpS & an | |||
// authentication mechanism. | |||
{ | |||
// The database to use. See the C++DB connection string reference at: | |||
// http://cppcms.com/sql/cppdb/connstr.html | |||
// This is a MySQL example replace the {...} parts with your site's settings. | |||
"trafficctrl": { | |||
"db": "mysql:user={username};password={password};database={db_name};@pool_size=10;@opt_reconnect=1" | |||
// Root URI (path) within a site. If using the stand alone HTTP server this | |||
// needs to match "http.script". But in that case leave both "". | |||
"root_uri": "", // root path on the hosting site. "" = "/" | |||
}, | |||
// These are C++CMS engine settings. See here for details: | |||
// http://cppcms.com/wikipp/en/page/cppcms_1x_config | |||
// This is a stand alone HTTP server setup: | |||
// "service": { | |||
// "api" : "http", // fastcgi | |||
// "ip": "0.0.0.0", | |||
// "port" : 8080 | |||
// // or "socket": "path..." | |||
// }, | |||
// "http" : { "script": "" }, | |||
// This is a FastCGI example, which could be used to provide HTTPS & passwords | |||
// via the host service: | |||
"service": { | |||
"api" : "fastcgi", | |||
// To serve on a TCP socket: "ip": "0.0.0.0", "port": 8080 | |||
// or a socket. preferred if lesser privlieged users have access | |||
// to the server: | |||
"socket": "/run/poorman-ids/traffcctl.fcgi" | |||
}, | |||
// In context of restricted access environment this is probably OK. | |||
"security" : { | |||
"display_error_message": true, | |||
// This requires JFP patches applied to C++CMS | |||
//"email_error_message": "SysOp@domain.net", | |||
}, | |||
// This tells us to run in background, as a daemon and to send errors to | |||
// SysLog. You can run this at the console by remarking out everything below. | |||
// Well.. other than the final '}' | |||
"daemon": { | |||
"enable": true, | |||
"lock": "/run/poorman-ids/trafficctrl.pid", | |||
}, | |||
"logging": { | |||
"syslog": { | |||
"enable": true, | |||
"id": "trafficctrl" | |||
} | |||
} | |||
} |
@@ -0,0 +1,266 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Traffic Montor Control HTTP server | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started August 13th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// Provide a control panel to manage which domains we want to watch, | |||
// ignore and block. | |||
////////////////////////////////////////////////////////////////////// | |||
// TODO: put the note fields to use (dns,dns_wild) | |||
// TODO: Any purpose in wild-card selection "accepted" host names? | |||
#include <stdlib.h> | |||
#include <string> | |||
#include <stdexcept> | |||
#include <iostream> | |||
/// C++CMS/DB /// | |||
#include <cppcms/service.h> | |||
#include <cppcms/applications_pool.h> | |||
#include <cppcms/mount_point.h> | |||
#include <cppcms/url_dispatcher.h> | |||
#include <cppcms/url_mapper.h> | |||
#include <cppcms/http_request.h> | |||
#include <cppcms/http_response.h> | |||
#include <cppdb/frontend.h> | |||
/// Our app /// | |||
#include "../strutil.h" | |||
#include "data.h" | |||
/// Build flags /// | |||
// TODO: better application of C++CMS booster logging | |||
//#define DEBUGGIN | |||
////////////////////////////////////////////////////////////////////// | |||
// DNS wild card matcher | |||
////////////////////////////////////////////////////////////////////// | |||
bool dns_wild_match(const StringList &wilds, const std::string &host) { | |||
for(StringList::const_iterator i=wilds.begin(); i!=wilds.end(); i++) { | |||
if(host.length()<i->length()) continue; | |||
if(host==*i) return true; | |||
if(host.substr(host.length()-i->length()+1)=="."+*i) return true; | |||
} | |||
return false; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// C++CMS App to manage the state of all known domain names. | |||
////////////////////////////////////////////////////////////////////// | |||
const std::string filter_titles[] = { "Undecided", "Accepted", "Blocked" }; | |||
const std::string actions[] = { "Reset", "Accept", "Block" }; | |||
std::string root_uri = ""; | |||
struct app: public cppcms::application { | |||
std::auto_ptr<cppdb::session> sql; | |||
int items_per_page; | |||
std::string numlist(const std::string &field) { | |||
std::string r; | |||
const cppcms::http::request::form_type &POST = request().post(); | |||
for( | |||
cppcms::http::request::form_type::const_iterator it = POST.find(field); | |||
it!=POST.end() && it->first==field; | |||
it++ | |||
) { | |||
if(it->second!="") { | |||
if(r!="") r+=","; | |||
r+="'"+sql->escape(it->second)+"'"; | |||
} | |||
} | |||
return r; | |||
} | |||
/// This looks at new log entries and creates missing DNS entries. | |||
/// This should be run periodically. Right now we run just prior | |||
/// displaying a "list" page. This could be used in a CRON job... | |||
void catchup() { | |||
std::string s; | |||
StringList list; | |||
StringList wild; | |||
/// Read list of wild-card blocks /// | |||
cppdb::result r = *sql | |||
<< "SELECT name FROM dns_wild WHERE status=?" | |||
<< Domain::blocked; | |||
while(r.next()) { | |||
r >> s; | |||
wild.push_back(s); | |||
} | |||
/// Auto-add unknown domains to DNS list /// | |||
// Find all connections not recorded in the DNS table | |||
r = *sql << | |||
"SELECT c.them_name " | |||
"FROM connections c LEFT OUTER JOIN dns ON c.them_name=dns.name " | |||
"WHERE c.them_name<>'' AND dns.name IS NULL " | |||
"GROUP BY c.them_name"; | |||
while(r.next()) { | |||
r >> s; | |||
list.push_back(s); | |||
} | |||
// add them | |||
if(!list.empty()) { | |||
cppdb::statement q = *sql << "INSERT INTO dns (name, status) VALUES (?,?)"; | |||
for(StringList::iterator i=list.begin(); i!=list.end(); i++) { | |||
q.reset(); | |||
// If blocked by wild card add it to the blocked list otherwise its | |||
// undecided. | |||
q << *i << (Domain::blocked*dns_wild_match(wild, *i)) << cppdb::exec; | |||
} | |||
} | |||
} | |||
void list(Domain::STATUS sid) { | |||
int i; | |||
std::string s; | |||
DomainList c; | |||
cppdb::result r; | |||
// TODO: put this someplace else? | |||
/// Form processing /// | |||
if(request().request_method()=="POST") { | |||
std::string op = request().post("op"); | |||
// nothing to do without a valid "op" | |||
if(op=="0" || op=="1" || op=="2") { | |||
// eliminate NOP busywork | |||
if((s=numlist("id"))!="" && sid!=atoi(op.c_str())) { | |||
*sql << "UPDATE dns SET status="+op+" WHERE name IN ("+s+")" | |||
<< cppdb::exec; | |||
} | |||
if(s=="*" || s=="*.") { | |||
c.error = "'*' and '*.' are not acceptable."; | |||
} else if((s=request().post("domain"))!="") { | |||
// wild card block handling | |||
if(s.substr(0,2)=="*.") { | |||
s = s.substr(2); | |||
if(op=="2") | |||
*sql << "INSERT INTO dns_wild (name,status) VALUES (?,?)" | |||
<< s << op | |||
<< cppdb::exec; | |||
else | |||
c.error = "Wild cards can only be used to <b>block</b> domains. " | |||
"This has been treated as regular domain prefix " | |||
"search."; | |||
} | |||
// regardless move all existing matches to the specified status. | |||
*sql << "UPDATE dns SET status=? WHERE name=? OR name LIKE ?" | |||
<< op << s << ("%."+s) | |||
<< cppdb::exec; | |||
} | |||
} | |||
} | |||
/// Update DB with new log data /// | |||
catchup(); | |||
/// Produce list of names of the desired STATUS /// | |||
c.filter = filter_titles[sid]; | |||
s = request().get("pg"); | |||
if(s=="") | |||
c.page = 1; | |||
else | |||
c.page = atoi(s.c_str()); | |||
if(c.page < 1 || c.page > 999999) c.page = 1; | |||
r = *sql << "SELECT name, decided, status FROM dns WHERE status=? " | |||
"LIMIT "+str((c.page-1)*items_per_page)+","+str(items_per_page) | |||
<< sid; | |||
for(i = c.list.size(); r.next(); i++) { | |||
c.list.resize(i+1); | |||
r >> c.list[i].name >> c.list[i].decided >> c.list[i].status; | |||
} | |||
r = *sql << "SELECT count(*) FROM dns WHERE status=?" << sid << cppdb::row; | |||
r >> c.count; | |||
c.page_size = items_per_page; | |||
c.pages = (c.count+items_per_page-1)/items_per_page; | |||
render("mainskin", "domain_list", c); | |||
} | |||
void undecided() { list(Domain::undecided); } | |||
void accepted() { list(Domain::accepted ); } | |||
void blocked() { list(Domain::blocked ); } | |||
app(cppcms::service &s): cppcms::application(s), items_per_page(50) { | |||
#ifdef DEBUGGIN | |||
std::cerr << "spawning app object" << std::endl; | |||
#endif | |||
sql.reset(new cppdb::session()); | |||
sql->open(settings().get<std::string>("trafficctrl.db")); | |||
mapper().root(root_uri); | |||
mapper().assign("blocked", "/blocked"); | |||
dispatcher().assign("/blocked/?", &app::blocked, this); | |||
mapper().assign("accepted", "/accepted"); | |||
dispatcher().assign("/accepted/?", &app::accepted, this); | |||
mapper().assign(""); | |||
dispatcher().assign("/?", &app::undecided, this); | |||
} | |||
// logging | |||
void main(const std::string url) { | |||
#ifdef DEBUGGIN | |||
std::cerr << "request: " << url << '\n' | |||
<< " INFO: " << request().path_info() << '\n' | |||
<< " SCRIPT: " << request().script_name() << '\n' | |||
<< std::endl; | |||
#endif | |||
cppcms::application::main(url); | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// main() - Launch C++CMS with my test app on "/" | |||
////////////////////////////////////////////////////////////////////// | |||
int main(int argc, char **args) { | |||
// create server object | |||
cppcms::service srv(argc, args); | |||
// Get root URI from configuration file. | |||
root_uri = srv.settings().get("trafficctrl.root_uri", ""); | |||
// Mount our app in the server | |||
srv.applications_pool().mount( | |||
cppcms::create_pool<app>(), | |||
cppcms::mount_point(root_uri) | |||
); | |||
// Serve it! | |||
// TODO: log crashes. | |||
#ifdef DEBUGGIN | |||
std::cerr << "Launching" << std::endl; | |||
#endif | |||
srv.run(); | |||
return 0; | |||
} | |||
@@ -179,3 +179,112 @@ int ConnList::find(Conn &needle) { | |||
for(r=0; r<size(); r++) if((*this)[r]==needle) return r; | |||
return -1; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// LogAnalyzer | |||
////////////////////////////////////////////////////////////////////// | |||
LogAnalyzer::LogAnalyzer(): | |||
us(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-"); | |||
} | |||
bool LogAnalyzer::line(const std::string &in) { | |||
int ict=0; | |||
NameVal::iterator nvp; | |||
std::string name, address, s; | |||
/// setup /// | |||
if(!us) | |||
throw std::runtime_error("LogAnalyzer::line: us list is not assigned"); | |||
ln=in; | |||
/// 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" || ln[5]=="cached") { | |||
name = ln[6]; | |||
address = ln[8]; | |||
/* NOTE: CNAME resolution seems to follow this order in logs: | |||
1. A result line (reply/cached) with an address of <CNAME> | |||
2. One or more consecutive result lines for the canonical name | |||
Looking over the logs it doesn't appear that dnsmasq will log | |||
anything between the original and CNAME resolutions. The exception | |||
is if a CNAME record is cached and it has to resolve what it | |||
points to. In this case there would be a "cached" and then a | |||
"forwarded" record eventually followed by "reply ... <CNAME>". | |||
In that case we want to operate on the reply. | |||
I just saw that CNAME log entries can be chained. It looks like | |||
they are an "is <CNAME>" entry followed by another. We want to | |||
keep the original name (alias). | |||
*/ | |||
// we're handling a CNAME entry | |||
if(address=="<CNAME>") { | |||
// If we don't have a cname yet then this is a CNAME to a CNAME. | |||
if(alias=="" || cname!="") { | |||
alias = name; | |||
cname = ""; | |||
} | |||
return 0; | |||
} | |||
// If in cname _mode_: | |||
if(alias!="") { | |||
if(cname=="") { | |||
cname = name; // This is our target name | |||
name = alias; // substitute the alias | |||
} else if(cname==name) { | |||
name = alias; // substitute the alias | |||
} else { | |||
cname = ""; // These are different records reset | |||
name = ""; | |||
} | |||
} | |||
// Hmm... is this reply an address? | |||
if(pre_match(dns_ignore, address)) return 0; // nope | |||
if(pre_match(dns_del, address)) return 0; // does not exist reply | |||
if((nvp=rdns.find(address))!=rdns.end()) { | |||
if(nvp->second==name) return 0; | |||
//dlog("WARN: DNS address overlap "+address+": "+nvp->second+" : "+name); | |||
} | |||
rdns[address] = name; | |||
//dlog("Added "+address+" = "+name); | |||
return 0; | |||
} else if(alias!="") { | |||
// we've fallen out of CNAME resolution. | |||
alias = ""; | |||
cname = ""; | |||
} | |||
} | |||
/// process connections /// | |||
if((ln.count>5 // old Linux style | |||
&& ln[4]=="kernel:" | |||
&& ln[5]=="ACCEPT" | |||
) || (ln.count>6 // new Linux style | |||
&& ln[4]=="vmunix:" | |||
&& ln[6]=="ACCEPT") | |||
) { | |||
conn = ln; | |||
conn.compact(); | |||
if(!pre_match(*us, conn.us)) conn.swap(); | |||
if((nvp=rdns.find(conn.them))!=rdns.end()) conn.name = nvp->second; | |||
return 1; | |||
} | |||
return 0; | |||
} |
@@ -13,6 +13,7 @@ | |||
#ifndef __JFP_IPTRAFFIC_DATA_H__ | |||
#define __JFP_IPTRAFFIC_DATA_H__ | |||
#include <string> | |||
#include <istream> | |||
#include <ostream> | |||
#include <vector> | |||
#include "strutil.h" | |||
@@ -89,4 +90,25 @@ struct ConnList: public std::vector<Conn> { | |||
////////////////////////////////////////////////////////////////////// | |||
// Log Analyzer | |||
////////////////////////////////////////////////////////////////////// | |||
struct LogAnalyzer { | |||
StringList *us; | |||
StringList dns_ignore, // DNS response prefixes to ignore | |||
dns_del; // DNS response prefixes to /delete/ (ignore) | |||
NameVal rdns; // Reverse DNS lookup cache | |||
Conn conn; // Last connection worked on | |||
Splits ln; // Work buffer for line processing | |||
std::string alias; // The name requiring CNAME resolution | |||
std::string cname; // The cname alias was pointing to. | |||
LogAnalyzer(); | |||
// Process a log line. Returns "true" if it were a netfilter entry. | |||
bool line(const std::string &in); | |||
}; | |||
#endif |
@@ -23,10 +23,12 @@ | |||
// - Getting input and output filenams from CLI args | |||
// - Reading and writing from STDIN & STDOUT | |||
// - Send all non-data output to stderr | |||
// | |||
// 2021-08-11 <ChipMaster@YeOlPiShack.net> | |||
// Move main data colation routine into its own class to be shared | |||
// with multiple tools. | |||
////////////////////////////////////////////////////////////////////// | |||
// TODO: map names according to time and requesting host. time is probably automatic | |||
#include <string.h> | |||
#include <string> | |||
#include <iostream> | |||
@@ -49,8 +51,7 @@ using namespace std; | |||
struct IPtraffic: public cBaseApp { | |||
Config config; | |||
StringList dns_ignore, dns_del; | |||
NameVal rdns; // Reverse DNS lookup cache | |||
LogAnalyzer analyze; | |||
istream *log; | |||
ostream *out; | |||
LiveBug bug; | |||
@@ -60,10 +61,7 @@ struct IPtraffic: public cBaseApp { | |||
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-"); | |||
analyze.us = &(config.us.vals); | |||
} | |||
@@ -80,11 +78,11 @@ struct IPtraffic: public cBaseApp { | |||
// TODO: elaborate | |||
void help() { | |||
int help() { | |||
cerr << | |||
"\n" | |||
"iptraffic -c {config file} [-o {output file}] [{input file} [...]]\n"; | |||
ExitCode = 1; | |||
return ExitCode = 1; | |||
} | |||
@@ -100,7 +98,7 @@ struct IPtraffic: public cBaseApp { | |||
switch(*sw) { | |||
case 'c': | |||
config.load(val); | |||
if(!config.us.size()) throw CLIerror( | |||
if(!config.us.vals.size()) throw CLIerror( | |||
"The configuration files MUST contain an [us] section with " | |||
"appropriate values" | |||
); | |||
@@ -127,69 +125,33 @@ struct IPtraffic: public cBaseApp { | |||
// 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; | |||
int ict=0; // ignored netfilter lines | |||
std::string l; | |||
/// parse log file /// | |||
if(!config.us.vals.size()) throw CLIerror( | |||
"A configuration file must be specified before input files." | |||
); | |||
line_no=0; | |||
while((*log >> ln)) { | |||
while(std::getline(*log, l)) { | |||
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"; | |||
if(analyze.line(l)) { | |||
if(config.ignores.vals.find(analyze.conn)<0) | |||
*out << analyze.ln[0] << " " << analyze.ln[1] << " " << analyze.ln[2] | |||
<< " " << analyze.conn << "\n"; | |||
else | |||
ict++; | |||
} | |||
} | |||
*out << flush; // make sure all data gets written. | |||
cerr << "\nIgnored: " << ict << endl; | |||
cerr << "Total rDNS: " << rdns.size() << "\n"; | |||
cerr << "Lines: " << line_no | |||
<< "\nIgnored: " << ict | |||
<< "\nTotal rDNS: " << analyze.rdns.size() << endl; | |||
return 0; | |||
} | |||
@@ -0,0 +1,129 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Mini INI File Crackers | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started June 1st, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// Used with permission in the "Poor Man's IDS" project | |||
////////////////////////////////////////////////////////////////////// | |||
#include <stdlib.h> | |||
#include <fstream> | |||
#include <stdexcept> | |||
#include "miniini.h" | |||
////////////////////////////////////////////////////////////////////// | |||
// MiniINIvars | |||
////////////////////////////////////////////////////////////////////// | |||
std::string MiniINIvars::get(const std::string &name, const std::string &def) { | |||
NameVal::iterator v = vals.find(name); | |||
if(v!=vals.end()) return v->second; | |||
vals[name]=def; | |||
return def; | |||
} | |||
std::string MiniINIvars::get(const std::string &name) const { | |||
NameVal::const_iterator v = vals.find(name); | |||
return v!=vals.end() ? v->second : ""; | |||
} | |||
// TODO: come up with better range checking to allow full range of 32bit ints. | |||
int MiniINIvars::geti(const std::string &name, int def, int min, int max) const { | |||
int r; | |||
NameVal::const_iterator v = vals.find(name); | |||
if(v!=vals.end()) { | |||
const std::string &s = v->second; | |||
if(s!="" && s.size()<=9+(s[0]=='+' || s[0]=='-')) { | |||
r = atoi(s.c_str()); | |||
if(r>=min && r<=max) return r; | |||
} | |||
} | |||
return def; | |||
} | |||
void MiniINIvars::add(const std::string &in) { | |||
std::string s = trim(in); | |||
std::string::size_type i; | |||
if(s=="" or s[0]=='#' or s[0]==';') return; // NOP | |||
i = s.find('='); | |||
if(i==s.npos || i==0) throw std::runtime_error( | |||
"MiniINIvars.add: incorrectly formatted name / value pair"); | |||
// TODO: catch dupe vars? | |||
vals[trim(s.substr(0, i-1))] = trim(s.substr(i+1)); | |||
} | |||
std::ostream &MiniINIvars::save(std::ostream &out) const { | |||
NameVal::const_iterator var; | |||
for(var = vals.begin(); var!=vals.end(); var++) | |||
out << var->first << " = " << var->second << '\n'; | |||
return out; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// MiniINIlines | |||
////////////////////////////////////////////////////////////////////// | |||
void MiniINIlines::add(const std::string &in) { | |||
vals.push_back(in); | |||
} | |||
std::ostream &MiniINIlines::save(std::ostream &out) const { | |||
StringList::const_iterator val; | |||
for(val = vals.begin(); val!=vals.end(); val++) out << *val << '\n'; | |||
return out; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// MiniINI | |||
////////////////////////////////////////////////////////////////////// | |||
void MiniINI::load(const std::string &fname) { | |||
std::string l; | |||
std::string gname; | |||
MiniINIgroup *group = 0; | |||
std::ifstream f(fname.c_str()); | |||
INIgroupList::iterator gmap; | |||
int ln=0; | |||
while(std::getline(f, l)) { | |||
ln++; | |||
gname = trim(l); | |||
if(gname.size()>2 && gname[0]=='[' && gname.end()[-1]==']') { | |||
gname = gname.substr(1, gname.size()-2); | |||
group = (gmap = groups.find(gname))==groups.end() ? 0 : gmap->second; | |||
continue; | |||
} | |||
try { | |||
if(group) group->add(l); | |||
} catch(const std::exception &e) { | |||
throw std::runtime_error("Error parsing "+fname+" line "+str(ln)+ | |||
" "+e.what()); | |||
} | |||
} | |||
} | |||
void MiniINI::save(const std::string &fname) { | |||
std::ofstream f(fname.c_str()); | |||
INIgroupList::iterator group; | |||
for(group = groups.begin(); group!=groups.end(); group++) { | |||
f << '[' << group->first << "]\n"; | |||
f << *group->second << '\n'; | |||
} | |||
} |
@@ -0,0 +1,136 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Mini INI File Crackers | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started June 1st, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// Used with permission in the "Poor Man's IDS" project | |||
// | |||
// This set of classes is to prvide easy access to reading config data | |||
// from INI files. I'm going to take some liberty with the origianl | |||
// M$ format and allow groups to parse specialized text formats, not | |||
// just name/value pairs. | |||
////////////////////////////////////////////////////////////////////// | |||
#ifndef __JFP_MINIINI_H__ | |||
#define __JFP_MINIINI_H__ | |||
#include <string> | |||
#include <ostream> | |||
#include "strutil.h" | |||
////////////////////////////////////////////////////////////////////// | |||
// The base abstract group class | |||
// | |||
// Subclass and override this class to create a parser for the kind of | |||
// data you want in your group. | |||
////////////////////////////////////////////////////////////////////// | |||
struct MiniINI; | |||
struct MiniINIgroup { | |||
// vals: By convention the content of the group | |||
// this is overriden to handle the parsing of lines in this group | |||
// it must ignore whitespace and remarks, if allowed in the group's | |||
// format. | |||
virtual void add(const std::string &in) = 0; | |||
// This is overridden to dump the group back out to a file | |||
virtual std::ostream &save(std::ostream &out) const = 0; | |||
// cause we need the destructor virtual too! | |||
virtual ~MiniINIgroup() {} | |||
}; | |||
inline std::ostream &operator<<(std::ostream &out, const MiniINIgroup &ini) { | |||
return ini.save(out); | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// The typical collection of name+value pairs | |||
////////////////////////////////////////////////////////////////////// | |||
struct MiniINIvars: public MiniINIgroup { | |||
NameVal vals; // name val pairs | |||
/// Get a value setting it to def if not found | |||
/// | |||
/// This looks for name in vals. If found that value is returned. Otherwise | |||
/// the def is set in vals for the name. This means it will be saved at | |||
/// next writing. Def defaults to empty string. If you don't want to auto- | |||
/// create the value in the config file then use the appropriate methods of | |||
/// vals. | |||
virtual std::string get(const std::string &name, const std::string &def); | |||
/// Get a value NOT adding it to vals if not found | |||
/// | |||
/// This looks for name in vals. If found that value is returned. Otherwise | |||
/// "" is returned but the name is not added to vals. | |||
virtual std::string get(const std::string &name) const; | |||
/// Get an int from vals | |||
/// | |||
/// This will convert the string value, if found in vals, to an int and make | |||
/// sure its within bounds. If its not in bounds or doesn't exist or is an | |||
/// an empty string them def is returned. The vals is not modified. | |||
/// | |||
/// For safety values are limited to 9 digits plus sign, since this is a safe | |||
/// range for converting to 32 bit ints. | |||
virtual int geti(const std::string &name, int def=0, int min=-999999999, int max=999999999) const; | |||
/// parse an INI line into a name value pair | |||
virtual void add(const std::string &in); | |||
/// write all name value pairs from vals to out | |||
virtual std::ostream &save(std::ostream &out) const; | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// A raw collection of lines in a group | |||
////////////////////////////////////////////////////////////////////// | |||
struct MiniINIlines: public MiniINIgroup { | |||
StringList vals; // the raw lines of this group | |||
virtual void add(const std::string &in);// add an incoming line | |||
virtual std::ostream &save(std::ostream &out) const; | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// The main INI container that contains all groups | |||
// | |||
// Fill groups with a list of group objects and their associated | |||
// names. Call load() to read the groups from an INI file. Only the | |||
// group with names specified in groups will be read. Their content | |||
// parsed according to the group's rules. save() will write it back | |||
// out again. No attempt to preserve whitespace and remarks is made. | |||
////////////////////////////////////////////////////////////////////// | |||
typedef std::map<std::string,MiniINIgroup*> INIgroupList; | |||
struct MiniINI { | |||
// This is a list of groups allowed in the INI and after load() those | |||
// groups will contain the data from the INI and they will be written | |||
// on save(). | |||
// | |||
// This must be initialized with MiniINIgroup* objects so that it knows | |||
// how to process groups. No attempt is made to manage memory allocation. | |||
INIgroupList groups; | |||
// Load the file and parse into groups[]. If a group is encountered that | |||
// was not specified in groups[] it is ignored, and will be lost on | |||
// save(). Same for pre-group content. | |||
virtual void load(const std::string &fname); | |||
// Save the groups[] into the file. No attempt is made to preserve | |||
// whitespace or remarks. This is the "mini INI". Preserving thoee | |||
// requires a more complicated and thus less "mini" implementation. | |||
virtual void save(const std::string &fname); | |||
// A virtual NOP destructor... because its recommended. | |||
virtual ~MiniINI() {} | |||
}; | |||
#endif |
@@ -0,0 +1,128 @@ | |||
# This is DPAK Debian packaging source | |||
# DPAK is an tool of JF Possibilities, Inc. Written by ChipMaster. | |||
Source: poorman-ids | |||
Priority: extra | |||
Section: unknown | |||
Maintainer: Jon Foster <jon@jfpossibilities.com> | |||
Homepage: https://yeolpishack.net/repos/ChipMaster/Poor-Mans-IDS/wiki | |||
Description: Poor Man's IDS | |||
A simple tool to alert you to unknown traffic on your network. | |||
Copyright: . | |||
(c) 2021 JF Possibilities, Inc. All rights reserved. | |||
Origin: JFP | |||
Packaged-For: JF Possibilities, Inc. | |||
changelog: | |||
(0.6-1j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* Change handling of CNAMEs to report the originally requested name. | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Mon, 21 Mar 2022 14:56:19 -0700 | |||
. | |||
(0.5-2j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
This is primarily a bug fix and testing release. | |||
. | |||
* Leave symbols in bins to see how C++CMS reports errors. | |||
* Don't allow "*." or "*" in the wild card entry field. It breaks | |||
stuff! | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Mon, 03 Jan 2022 14:22:30 -0800 | |||
. | |||
(0.5-1j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* Minor internal restructuring of CLI apps. | |||
* *NEW* domblacklist tool to make DNSmasq whole domain blocks. | |||
* Also added the iptraffic log CLI log analyzer | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Mon, 03 Jan 2022 14:22:30 -0800 | |||
. | |||
(0.4-3j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* *FIX* mounting on sub URL of a site via FastCGI | |||
* Set UMASK in trafficctrl init script for better default perms. | |||
* Improved doc remarks in "sample.js". | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Tue, 14 Sep 2021 13:35:22 -0700 | |||
. | |||
(0.4-2j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* Expanded sample trafficctrl configuration. | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Mon, 13 Sep 2021 11:52:58 -0700 | |||
. | |||
(0.4-1j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* Added standard setup stuff like "init.d" scripts, syslog conf, and | |||
improved sample configuration files. | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Thu, 08 Sep 2021 13:58:40 -0700 | |||
. | |||
(0.3-1j) unstable; urgency=low | |||
. | |||
** This is an alpha release ** | |||
. | |||
* Initial Debianization and release of tools: | |||
- Log catching daemon | |||
- fCGI / HTTP prioritization tool | |||
- report tool | |||
. | |||
-- Jon Foster <jon@jfpossibilities.com> Thu, 02 Sep 2021 10:58:43 -0700 | |||
. | |||
Build: sh | |||
make | |||
Clean: sh | |||
make distclean | |||
Package: poorman-ids | |||
Architecture: any | |||
# I think libssl is required by cppcms. libmysqlclient18 is probably cppdb | |||
Depends: libc6, libstdc++6, cppdb (>= 0.3.1-4), cppcms, libssl1.0.0 | |||
#Depends: [] | |||
Recommends: libmysqlclient18 | |||
Description: . | |||
Install: sh | |||
dpak install -sbin iptraffic trafficmon/trafficmon trafficmon/badtrafficrpt | |||
dpak install -sbin trafficmon/dnsblacklist trafficmon/domblacklist | |||
dpak install -sbin controlpanel/trafficctrl | |||
dpak strip | |||
dpak install -conf -subdir poorman-ids sample.conf controlpanel/sample.js | |||
mkdir -p "$DPAK_ROOT/etc/default" | |||
cp trafficmon/default "$DPAK_ROOT/etc/default/trafficmon" | |||
cp controlpanel/default "$DPAK_ROOT/etc/default/trafficctrl" | |||
mkdir -p "$DPAK_ROOT/etc/init.d" | |||
cp trafficmon/init "$DPAK_ROOT/etc/init.d/trafficmon" | |||
cp controlpanel/init "$DPAK_ROOT/etc/init.d/trafficctrl" | |||
mkdir -p "$DPAK_ROOT/etc/syslog.d" | |||
cp trafficmon/syslog "$DPAK_ROOT/etc/syslog.d/trafficmon" | |||
Finalize: sh | |||
# Clean up permissions in the packaged files & folders. | |||
chmod -R g-s "$DPAK_ROOT" | |||
chmod 700 "$DPAK_ROOT/etc/poorman-ids" | |||
chmod 600 "$DPAK_ROOT/etc/poorman-ids/"* | |||
chmod 644 "$DPAK_ROOT/etc/default/"* | |||
chmod 755 "$DPAK_ROOT/etc/init.d/"* | |||
PostInst: sh | |||
update-rc.d trafficmon defaults | |||
update-rc.d trafficctrl defaults | |||
service trafficmon start || true | |||
service trafficctrl start || true | |||
PreRm: sh | |||
# Shut off services so they aren't RAM resident after install | |||
service trafficmon stop || true | |||
service trafficctrl stop || true | |||
PostRm: sh | |||
update-rc.d trafficmon remove | |||
update-rc.d trafficctrl remove |
@@ -0,0 +1,32 @@ | |||
# This is a sample config file. A single file can be used as repository of | |||
# information for all of the tools in this package, other than "trafficctrl", | |||
# since its a C++CMS app and C++CMS needs JSON to setup its features. | |||
# List of address prefixes that represent our networks (us) | |||
[us] | |||
192.168.1. | |||
# Traffic monitor (trafficmon) settings | |||
[Traffic Mon] | |||
db user = | |||
db password = | |||
db name = | |||
# db host = | |||
# Sample List of connections to ignore. So far this is only used by iptraffic, | |||
# the CLI log processor. Trafficmon and the badtrafficrpt use data in the DB. | |||
[ignores] | |||
# muttering to self | |||
127.0.0.1 0 127.0.0.1 0 * ICMP 0 | |||
127.0.0.1 0 127.0.0.1 0 * UDP 0 | |||
127.0.0.1 0 127.0.0.1 0 * TCP 0 | |||
127.0.0.1 0 127.0.0.1 0 * ICMP 1 | |||
127.0.0.1 0 127.0.0.1 0 * UDP 1 | |||
127.0.0.1 0 127.0.0.1 0 * TCP 1 | |||
::1 53 ::1 0 * UDP 1 | |||
::1 53 ::1 0 * TCP 1 |
@@ -1,10 +1,12 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// String splitter | |||
// String splitter & other useful string tools | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started April 23rd, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// Copied with permission from JF Possibilities's C++ lib. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string.h> | |||
#include <stdio.h> | |||
#include <stdexcept> | |||
// Sounds an awful lot like a German pastry | |||
#include "strutil.h" | |||
@@ -12,6 +14,42 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Generic string transformations | |||
////////////////////////////////////////////////////////////////////// | |||
std::string trim(const std::string &s) { | |||
int x, y; | |||
for(x=0; x<s.size() && s[x]<=' '; x++); | |||
for(y=s.size()-1; y>=x && s[y]<=' '; y--); | |||
if(y<x) return ""; | |||
return s.substr(x, y-x+1); | |||
} | |||
// You have to have C++11+ to get to_string() | |||
std::string str(long long n) { | |||
char s[24]; s[23]=0; | |||
snprintf(s, 23, "%lld", n); | |||
return std::string(s); | |||
} | |||
// TODO: more optimal way to handle? | |||
std::string qesc(const std::string &s) { | |||
std::string r="'"; | |||
int i; | |||
for(i=0; i<s.size(); i++) { | |||
if(s[i]=='\'') r+= "''"; else r+=s[i]; | |||
} | |||
r+='\''; | |||
return r; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// Splits | |||
////////////////////////////////////////////////////////////////////// | |||
@@ -3,8 +3,7 @@ | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started April 23rd, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// This is useful for breaking a text file line into fields. | |||
// Copied with permission from JF Possibilities's C++ lib. | |||
// | |||
// 2021-05-14 <ChipMaster@YeOlPiShack.net> | |||
// Restructure: broke out of monolithic iptraffic.cpp and made its | |||
@@ -28,6 +27,17 @@ typedef std::map<std::string,std::string> NameVal; | |||
////////////////////////////////////////////////////////////////////// | |||
// Generic string transformations | |||
////////////////////////////////////////////////////////////////////// | |||
std::string trim(const std::string &s); | |||
std::string str(long long n); | |||
// "query" escape: ' -> '' | |||
std::string qesc(const std::string &s); | |||
////////////////////////////////////////////////////////////////////// | |||
// Splits: a util class to divide a line into space sep pieces | |||
////////////////////////////////////////////////////////////////////// | |||
// TODO: implement begin() + end() to make "for( : )" work | |||
@@ -73,7 +83,7 @@ std::istream &operator>>(std::istream &in, Splits &sp); | |||
struct TSV: public Splits { | |||
TSV() { sep='\t'; combine=false; } | |||
// Need some weird casties to make C++ remember its base class | |||
TSV(const std::string &_line): Splits(_line) {} | |||
TSV(const std::string &_line) { sep='\t'; combine=false; *this=_line; } | |||
inline TSV &operator=(const std::string &_line) { return *this=_line.c_str(); } | |||
inline TSV &operator=(const char *_line) { *((Splits *)this)=_line; return *this; } | |||
}; | |||
@@ -0,0 +1 @@ | |||
/data |
@@ -0,0 +1,8 @@ | |||
data: data.cpp ../data.o testit.o | |||
g++ -o $@ $@.cpp ../data.o testit.o | |||
../data.o: ../data.cpp ../data.h | |||
cd .. && make data.o | |||
testit.o: testit.cpp testit.h | |||
g++ -c testit.cpp |
@@ -0,0 +1,81 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Test "data" module | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started June 28th, 2021 | |||
// | |||
// NOTE: This is really incomplete. More tests to come. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string> | |||
#include <iostream> | |||
#include <stdio.h> | |||
#include "testit.h" | |||
#include "../data.h" | |||
////////////////////////////////////////////////////////////////////// | |||
// TestIt Jig | |||
////////////////////////////////////////////////////////////////////// | |||
TestIt test; | |||
////////////////////////////////////////////////////////////////////// | |||
// Test 1 - Wild Card Address Comparisons | |||
////////////////////////////////////////////////////////////////////// | |||
/// Table /// | |||
struct WildAddrTest { | |||
std::string addr1, addr2; | |||
int result; | |||
}; | |||
const int wild_addr_tests_ct = 13; | |||
WildAddrTest wild_addr_tests[wild_addr_tests_ct] = { | |||
// IPv4 | |||
{"192.168.255." , "192.168.255.7", 0}, | |||
{"192.168.255.7", "192.168.255." , 0}, | |||
{"192.168.255.7", "192.168.255.7", 0}, | |||
{"192.168.255.7", "192.168.255.8", -1}, | |||
{"192.168.254.7", "192.168.255." , -1}, | |||
{"192.168.256.7", "192.168.255." , 1}, | |||
{"*" , "192.168.255." , 0}, | |||
{"192.168.256.7", "*" , 0}, | |||
//IPv6 | |||
{"2001:0470:000a:0169:" , "2001:0470:000a:0169:0000:0000:0000:0001", 0}, | |||
{"2001:0470:000a:0169:0000:0000:0000:0002", "2001:0470:000a:0169:" , 0}, | |||
{"2001:0470:000a:0170:" , "2001:0470:000a:0169:0000:0000:0000:0003", 1}, | |||
{"2001:0470:000a:0168:0000:0000:0000:0004", "2001:0470:000a:0169:" , -1}, | |||
{"2001:0470:000a:0169:0000:0000:0000:0001", "2001:0470:000a:0169:0000:0000:0000:0001", 0}, | |||
}; | |||
bool wild_addr_test() { | |||
int i; | |||
bool ok = true; | |||
char s[256]; s[255]=0; | |||
test.module("Wild Card Address Match"); | |||
for(i=0; i<wild_addr_tests_ct; i++) { | |||
snprintf(s, 255, "Test match %3d", i); | |||
if(!test.test(s, | |||
addr_wild_comp(wild_addr_tests[i].addr1, wild_addr_tests[i].addr2), | |||
wild_addr_tests[i].result | |||
)) | |||
ok = false; | |||
} | |||
return ok; | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// Lets do it | |||
////////////////////////////////////////////////////////////////////// | |||
int main() { | |||
wild_addr_test(); | |||
return !test.report(); | |||
} |
@@ -0,0 +1,52 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Test Jig | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started June 28th, 2021 | |||
// Copyright 2021 JF Possibilities, Inc. All Rights Reserved | |||
////////////////////////////////////////////////////////////////////// | |||
#include <iostream> | |||
#include "testit.h" | |||
using namespace std; | |||
////////////////////////////////////////////////////////////////////// | |||
// TestIt | |||
////////////////////////////////////////////////////////////////////// | |||
const string bools[2] = { | |||
vt100::RED+"FAIL"+vt100::RST, vt100::GRN+"ok"+vt100::RST | |||
}; | |||
void TestIt::module(const string &title) { | |||
cout << "\n" | |||
<< vt100::CYN << title << ":\n" | |||
<< string(title.size()+1, '=') << vt100::RST << endl; | |||
} | |||
bool TestIt::test(const string &title, int result, int match) { | |||
bool r = result==match; | |||
cout << vt100::YLW << title << '\t' << bools[r] << endl; | |||
count++; | |||
if(r) passes++; | |||
else fails.push_back(title); | |||
return r; | |||
} | |||
int TestIt::report() { | |||
int r = 0; | |||
if(fails.size()==count) r = 3; // Oh! That's miserable | |||
else if(fails.size()) r = 2; // something worked. | |||
cout << "\n" | |||
<< vt100::CYN << "RESULTS: Tests " << (r ? vt100::YLW : vt100::GRN) << count | |||
<< vt100::CYN << " Passes " << vt100::GRN << passes | |||
<< vt100::CYN << " Fails " << (r ? vt100::RED : vt100::GRN) << fails.size() | |||
<< vt100::RST << "\n"; | |||
return r; | |||
} |
@@ -0,0 +1,51 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Test Jig | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started June 28th, 2021 | |||
// Copyright 2021 JF Possibilities, Inc. All Rights Reserved | |||
// Included by permission from JF Possibilities, Inc. | |||
// | |||
// Simple framework for running tests and reporting. Mostly this is an | |||
// output formatter but also tracks stats on the tests reported to it. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string> | |||
#include <vector> | |||
////////////////////////////////////////////////////////////////////// | |||
// Pretty basic mechanism of formatting test output and performing | |||
// pass / fail accounting. | |||
////////////////////////////////////////////////////////////////////// | |||
struct TestIt { | |||
int count; // total count of calls to test() | |||
int passes; // total test() calls that matched | |||
std::vector<std::string> fails; // list of test titles that failed | |||
TestIt(): count(0), passes(0) {}; | |||
void module(const std::string &title);// Start a new test module | |||
bool test(const std::string &title, int result, int match); // Record test result | |||
int report(); // Print summary report and return: 0 passed, 2 some failed, 3 compete fail, suitable for return from main() | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// VT100 terminal control code constants | |||
// | |||
// Probably should learn to use termcap / curses. | |||
// | |||
// COLORS! | |||
////////////////////////////////////////////////////////////////////// | |||
namespace vt100 { | |||
const std::string | |||
RED = "\e[31;1m", | |||
YLW = "\e[33;1m", | |||
GRN = "\e[32m", | |||
GRY = "\e[2m", | |||
CYN = "\e[36m", | |||
RST = "\e[0m"; | |||
} |
@@ -0,0 +1,113 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Base CLI app classes for TrafficMon tools | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started December 29th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <stdexcept> | |||
#include <iostream> | |||
#include <libgen.h> | |||
#include "appbase.h" | |||
////////////////////////////////////////////////////////////////////// | |||
// TrafficMonBaseApp | |||
////////////////////////////////////////////////////////////////////// | |||
cBaseApp &TrafficMonBaseApp::init(int argc, char **argv) { | |||
if(!config) config = new MonitorBaseConf; | |||
return cBaseApp::init(argc, argv); | |||
} | |||
unsigned TrafficMonBaseApp::do_switch(const char *arg) { | |||
if(!arg[1] && *arg=='c') return 1; | |||
return cBaseApp::do_switch(arg); | |||
} | |||
void TrafficMonBaseApp::do_switch_arg(const char *sw, const std::string &val) { | |||
if(!sw[1] && *sw=='c') config->load(val); | |||
} | |||
int TrafficMonBaseApp::main() { | |||
int x; | |||
try { | |||
if(x=cBaseApp::main()) return x; // Parse CLI args | |||
if(!config->traffic_mon.vals.size()) throw CLIerror( | |||
"You need to load a config file with a [Traffic Mon] section" | |||
); | |||
} catch(const CLIerror &e) { | |||
std::cerr << e.what() << "\n\n"; | |||
return help(); | |||
} | |||
db.open("mysql:user="+qesc(config->traffic_mon.get("db user"))+ | |||
";password="+qesc(config->traffic_mon.get("db password"))+ | |||
";host="+qesc(config->traffic_mon.get("db host"))+ | |||
";database="+qesc(config->traffic_mon.get("db name"))+ | |||
";@opt_reconnect=1"); | |||
return 0; | |||
} | |||
TrafficMonBaseApp::~TrafficMonBaseApp() { | |||
if(config) delete(config); | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// BlackListBaseApp | |||
////////////////////////////////////////////////////////////////////// | |||
int BlackListBaseApp::help() { | |||
std::cerr << " FORMAT: " << basename(command_args[0]) << " -c {config} [-4 {address}] [-6 {address}]\n" | |||
<< '\n' | |||
<< "The config file must have a [Traffic Mon] section with the database\n" | |||
<< "credentials in it. -4 & -6 set the addresses to pin blocked names to.\n" | |||
<< "They default to the 'localhost' address in the respective family. Set\n" | |||
<< "to '' to turn off output of that family." << std::endl; | |||
return ExitCode = 1; | |||
} | |||
unsigned BlackListBaseApp::do_switch(const char *arg) { | |||
if(!arg[1] && (*arg=='4' || *arg=='6')) return 1; | |||
return TrafficMonBaseApp::do_switch(arg); | |||
} | |||
void BlackListBaseApp::do_switch_arg(const char *sw, const std::string &val) { | |||
if(!sw[1]) switch(*sw) { | |||
case '4': ipv4 = val; return; | |||
case '6': ipv6 = val; return; | |||
} | |||
TrafficMonBaseApp::do_switch_arg(sw, val); | |||
} | |||
void BlackListBaseApp::do_arg(const char *arg) { | |||
throw CLIerror("Invalid arguments"); | |||
} | |||
int BlackListBaseApp::main() { | |||
int x; | |||
if(x=TrafficMonBaseApp::main()) return x; // Parse CLI args, open conf & db | |||
if(ipv4=="" && ipv6=="") { | |||
std::cerr << "All address families turned off. Nothing to do." << std::endl; | |||
return 1; | |||
} | |||
return 0; | |||
} |
@@ -0,0 +1,85 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Base CLI app classes for TrafficMon tools | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started December 29th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// | |||
////////////////////////////////////////////////////////////////////// | |||
// NOTE: since GNU doesn't discard unused classes these two classes should | |||
// probably get put in separate sets of files. :-/ | |||
#ifndef __IDS_MONITOR_BASE_APP_H__ | |||
#define __IDS_MONITOR_BASE_APP_H__ | |||
#include <cppdb/frontend.h> | |||
#include "../cli.h" | |||
#include "../miniini.h" | |||
////////////////////////////////////////////////////////////////////// | |||
// The core configuration file | |||
// | |||
// This is designed so that all parts can use the same config. Tools | |||
// ignore the parts they aren't interested in. | |||
////////////////////////////////////////////////////////////////////// | |||
struct MonitorBaseConf: public MiniINI { | |||
MiniINIvars traffic_mon; // This app's config variables | |||
MonitorBaseConf() { groups["Traffic Mon"] = &traffic_mon; } | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// The base CLI application class used by the tools in this directory. | |||
// | |||
// Essentially this is a CLI app with a DB connection and a place | |||
// holder for a config file. | |||
////////////////////////////////////////////////////////////////////// | |||
struct TrafficMonBaseApp: public cBaseApp { | |||
cppdb::session db; | |||
MonitorBaseConf *config; | |||
// this init() will create a MonitorBaseConf if a config hasn't been assigned. | |||
virtual cBaseApp &init(int argc, char **argv); | |||
// process config file switch and load the file | |||
virtual unsigned do_switch(const char *arg); | |||
virtual void do_switch_arg(const char *sw, const std::string &val); | |||
// process CLI args, test for [traffic mon] and connect to DB. | |||
virtual int main(); | |||
// close out and free config object. | |||
virtual ~TrafficMonBaseApp(); | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Blacklist report base class | |||
// | |||
// This provides generic switch handling | |||
////////////////////////////////////////////////////////////////////// | |||
struct BlackListBaseApp: public TrafficMonBaseApp { | |||
std::string ipv4, ipv6; | |||
BlackListBaseApp(): | |||
ipv4("127.0.0.1"), | |||
ipv6("::1") | |||
{} | |||
// Display generic CLI help text | |||
virtual int help(); | |||
// process -4 & -6 switches. | |||
virtual unsigned do_switch(const char *sw); | |||
virtual void do_switch_arg(const char *sw, const std::string &val); | |||
virtual void do_arg(const char *arg); | |||
virtual int main(); | |||
}; | |||
#endif |
@@ -0,0 +1,209 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Basic Access Log report | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started August 20th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// To start with this gather data for a specific access period and | |||
// create a report of domain names and addresses that accessed to and | |||
// from the net. All "accepted" and "blocked" accesses will be | |||
// ignored. For the moment we're expecting blocked traffic is actually | |||
// blocked. | |||
// | |||
// The report will contain three columns: | |||
// 1. domain name or address if a domain is not known. | |||
// 2. list of ports that were connected to. | |||
// 3. count of total connections | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string> | |||
#include <map> | |||
#include <iostream> | |||
#include <stdio.h> | |||
#include <libgen.h> | |||
#include "../strutil.h" | |||
#include "appbase.h" | |||
using namespace std; | |||
////////////////////////////////////////////////////////////////////// | |||
// A "line" of a report - a single domain/address | |||
////////////////////////////////////////////////////////////////////// | |||
struct ReportLine { | |||
int count; | |||
map<string,int> ports; | |||
// initialize | |||
ReportLine(): count(0) {} | |||
// add an item | |||
void add(const string &port, int _count) { | |||
ports[port]+=_count; | |||
count+=_count; | |||
} | |||
// ports -> string | |||
string port_list() const { | |||
string r; | |||
map<string,int>::const_iterator it = ports.begin(); | |||
if(it==ports.end()) return r; | |||
r = it->first; | |||
for(it++; it!=ports.end(); it++) r+=","+it->first; | |||
return r; | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// A full reports worth of data - domain names / addresses and their | |||
// associated stats. | |||
////////////////////////////////////////////////////////////////////// | |||
struct ReportData: map<string,ReportLine> { | |||
// Add an item to the report | |||
void add(const string &address, const string &port, int count) { | |||
(*this)[address].add(port, count); | |||
} | |||
// Render report in an ASCII table | |||
string ascii() const { | |||
int widths[3] = {0,0,0}; // max column widths: 0: DNS, 1: ports, 3: counts | |||
int x; | |||
char l[256]; l[255]=0; | |||
string s, r, bk; | |||
ReportData::const_iterator it; | |||
for(it = begin(); it!=end(); it++) { | |||
if(it->first.size()>widths[0]) widths[0] = it->first.size(); | |||
s = it->second.port_list(); | |||
if(s.size()>widths[1]) widths[1]=s.size(); | |||
if(it->second.count>widths[2]) widths[2] = it->second.count; | |||
} | |||
// Now conver count max to cols | |||
for(x=0; widths[2]; x++) widths[2] /= 10; | |||
widths[2] = x ? x : 1; | |||
// min col widths for titles | |||
if(widths[0]<6) widths[0]=6; | |||
if(widths[1]<7) widths[1]=7; | |||
if(widths[2]<2) widths[2]=2; | |||
// render report | |||
bk = "+"+string(widths[0]+2, '-')+ | |||
"+"+string(widths[1]+2, '-')+ | |||
"+"+string(widths[2]+2, '-')+ | |||
"+\n"; | |||
s = "| %-"+str(widths[0])+"s | %-"+str(widths[1])+"s | %"+str(widths[2])+"d |\n"; | |||
r = bk; | |||
snprintf(l, sizeof(l)-1, | |||
("| %-"+str(widths[0])+"s | %-"+str(widths[1])+"s | %-"+str(widths[2])+"s |\n").c_str(), | |||
(const char *)"Remote", | |||
(const char *)"Port(s)", | |||
(const char *)"Ct" | |||
); | |||
r+= l; | |||
r+= bk; | |||
for(it = begin(); it!=end(); it++) { | |||
snprintf(l, sizeof(l)-1, s.c_str(), | |||
it->first.c_str(), | |||
it->second.port_list().c_str(), | |||
it->second.count | |||
); | |||
r+=l; | |||
} | |||
r+=bk; | |||
return r; | |||
} | |||
}; | |||
inline ostream &operator<<(ostream &out, const ReportData &r){ | |||
return out << r.ascii(); | |||
} | |||
namespace cppdb { | |||
result &operator>>(result &qry, ::ReportData &rpt) { | |||
string name, addr, port; | |||
int ct; | |||
qry >> name >> addr >> port >> ct; | |||
if(name=="") name=addr; | |||
rpt.add(name, port, ct); | |||
} | |||
} | |||
////////////////////////////////////////////////////////////////////// | |||
// Connection Report Generator Application Class | |||
////////////////////////////////////////////////////////////////////// | |||
struct appConnectionReport: TrafficMonBaseApp { | |||
ReportData rpt; | |||
string start_stamp; | |||
string end_stamp; | |||
int cli_mode; // which non-switch are we processing | |||
appConnectionReport(): cli_mode(0) { } | |||
int help() { | |||
cerr << " FORMAT: " << basename(command_args[0]) << " -c {config} {start} {end}\n" | |||
<< '\n' | |||
<< "The config file must have a [Traffic Mon] section with the database\n" | |||
<< "credentials in it. 'start' and 'stop' are the SQL timestamps for\n" | |||
<< "report time span." << endl; | |||
return 1; | |||
} | |||
virtual void do_arg(const char *arg) { | |||
switch(cli_mode++) { | |||
case 0: start_stamp = arg; return; | |||
case 1: end_stamp = arg; return; | |||
default: throw CLIerror("Invalid arguments"); | |||
} | |||
} | |||
int main() { | |||
cppdb::result qry; | |||
int x; | |||
/// SETUP & VALIDATE CLI /// | |||
try { | |||
if(x = TrafficMonBaseApp::main()) return x; // Parse CLI args | |||
if(cli_mode!=2) throw CLIerror("Invlaid arguments"); | |||
} catch(const CLIerror &e) { | |||
cerr << e.what() << "\n\n"; | |||
return help(); | |||
} | |||
/// Query & load data /// | |||
qry = db << | |||
"SELECT c.them_name, c.them, c.them_port, count(*) " | |||
"FROM connections c LEFT OUTER JOIN dns d ON c.them_name=d.name " | |||
"WHERE c.inbound=0 AND (d.status IS NULL or d.status=0) " | |||
"AND tstamp>=? AND tstamp<=? " | |||
"GROUP BY c.them_name, c.them, c.them_port" | |||
<< start_stamp << (end_stamp + " 23:59:59"); // include to the end of the day | |||
while(qry.next()) qry >> rpt; | |||
/// spit out the report /// | |||
cout << "Web access report for " << start_stamp << " - " << end_stamp << "\n\n" | |||
<< rpt; | |||
return 0; | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Lets run the report and dump it out | |||
////////////////////////////////////////////////////////////////////// | |||
MAIN(appConnectionReport) |
@@ -0,0 +1,10 @@ | |||
# Configuration for init.d/trafficmon. All entries remarked out below are the | |||
# defaults. | |||
# Configuration file for the TrafficMon server | |||
# NOTE: service won't start until this is set | |||
#CONF=/etc/poorman-ids/sample.conf | |||
# Where "run" files are placed. This is the Debian+ default: | |||
#RUN=/run | |||
# This needs to match the pipe speicified in the syslog.d/trafficmon file. | |||
#SOCK=$RUN/poorman-ids/trafficmon.sock |
@@ -0,0 +1,139 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Dump Black Listed DNS entries | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started Ocotber 27th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// Read the "dns" table and dump all black listed host names as | |||
// entries for a "hosts" file. This could also be easily done with a | |||
// script but I want to be able to use the same config file as every- | |||
// thing else and parsing in SH is clumsy at best. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string> | |||
#include <map> | |||
#include <iostream> | |||
#include <stdio.h> | |||
#include <libgen.h> | |||
#include "../strutil.h" | |||
#include "appbase.h" | |||
using namespace std; | |||
////////////////////////////////////////////////////////////////////// | |||
// Class to manage and test hoset names agains bad domains | |||
////////////////////////////////////////////////////////////////////// | |||
struct DomainList: public StringList { | |||
bool operator==(const std::string host) { | |||
DomainList::const_iterator i; | |||
int dl, hl = host.size(); | |||
for(i=begin(); i!=end(); i++) { | |||
if(*i==host) return true; | |||
dl = i->size()+1; | |||
if(hl>dl && host.substr(hl-dl)=="."+*i) return true; | |||
} | |||
return false; | |||
} | |||
inline bool operator!=(const std::string host) { return !(*this==host); } | |||
}; | |||
namespace cppdb { | |||
session &operator>>(cppdb::session &db, DomainList &doms) { | |||
cppdb::result qry; | |||
std::string s; | |||
doms.clear(); | |||
qry = db << "SELECT name FROM dns_wild WHERE status=2"; | |||
while(qry.next()) { | |||
qry >> s; | |||
doms.push_back(s); | |||
} | |||
return db; | |||
} | |||
} // cppdb | |||
////////////////////////////////////////////////////////////////////// | |||
// Connection Report Generator Application Class | |||
////////////////////////////////////////////////////////////////////// | |||
struct DNSblackList: BlackListBaseApp { | |||
bool all; | |||
unsigned do_switch(const char *arg) { | |||
if(*arg=='a' && !arg[1]) { all=1; return 0; } | |||
return BlackListBaseApp::do_switch(arg); | |||
} | |||
int help() { | |||
std::cerr << " FORMAT: " << basename(command_args[0]) << " -c {config} [-a] [-4 {address}] [-6 {address}]\n" | |||
<< '\n' | |||
<< "The config file must have a [Traffic Mon] section with the database\n" | |||
<< "credentials in it. -4 & -6 set the addresses to pin blocked names to.\n" | |||
<< "They default to the 'localhost' address in the respective family. Set\n" | |||
<< "to '' to turn off output of that family. -a dumps all blocked host\n" | |||
<< "names otherwise host names that are covered by a domain block will\n" | |||
<< "not be shown." << std::endl; | |||
return ExitCode = 1; | |||
} | |||
int main() { | |||
DomainList baddoms; | |||
cppdb::result qry; | |||
string s; | |||
int x; | |||
/// SETUP & VALIDATE CLI /// | |||
all = false; | |||
if(x=BlackListBaseApp::main()) return x; // Parse CLI args, open conf & db | |||
if(ipv6!="" && ipv6.size()<8) ipv6+='\t'; // an extra \t to line up columns. :-) | |||
/// Load list of bad domains /// | |||
// These should be excluded from the list below since they should be | |||
// blocked by other means and the point of domain wide blocking is to | |||
// relieve the burden on the blocking tools (dnsmasq). | |||
if(!all) db >> baddoms; | |||
/// Query & load data /// | |||
qry = db << | |||
"SELECT name " | |||
"FROM dns " | |||
"WHERE status=2 " // 2 = blocked... need this doc'd somewhere... | |||
"ORDER BY name"; | |||
while(qry.next()) { | |||
qry >> s; | |||
if(all || baddoms!=s) { // exclude blocked domains | |||
if(ipv4!="") cout << ipv4 << '\t' << s << '\n'; | |||
if(ipv6!="") cout << ipv6 << '\t' << s << '\n'; | |||
} | |||
} | |||
return 0; | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Lets run the report and dump it out | |||
////////////////////////////////////////////////////////////////////// | |||
MAIN(DNSblackList) |
@@ -0,0 +1,61 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// Dump Black Listed whole domain (*.domain.tld) entries | |||
// Written by Jonathan A. Foster <jon@jfpossibilities.com> | |||
// Started December 28th, 2021 | |||
// Copyright JF Possibilities, Inc. All rights reserved. | |||
// | |||
// Read the "dns_wild" table and dump all black listed domain names as | |||
// "address" entries for a dnsmasq.conf file. This will black list the | |||
// whole domain, subdomains, hosts and all. | |||
////////////////////////////////////////////////////////////////////// | |||
#include <string> | |||
#include <map> | |||
#include <iostream> | |||
#include <stdio.h> | |||
#include <libgen.h> | |||
#include "../strutil.h" | |||
#include "appbase.h" | |||
using namespace std; | |||
////////////////////////////////////////////////////////////////////// | |||
// Connection Report Generator Application Class | |||
////////////////////////////////////////////////////////////////////// | |||
struct DomainBlackList: BlackListBaseApp { | |||
int main() { | |||
cppdb::result qry; | |||
string s; | |||
int x; | |||
/// SETUP & VALIDATE CLI /// | |||
if(x=BlackListBaseApp::main()) return x; // Parse CLI args, open conf & db | |||
/// Query & load data /// | |||
qry = db << | |||
"SELECT name " | |||
"FROM dns_wild " | |||
"WHERE status=2 " // 2 = blocked... need this doc'd somewhere... | |||
"ORDER BY name"; | |||
while(qry.next()) { | |||
qry >> s; | |||
if(ipv4!="") cout << "address=/" << s << '/' << ipv4 << '\n'; | |||
if(ipv6!="") cout << "address=/" << s << '/' << ipv6 << '\n'; | |||
} | |||
return 0; | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Lets run the report and dump it out | |||
////////////////////////////////////////////////////////////////////// | |||
MAIN(DomainBlackList) |
@@ -0,0 +1,120 @@ | |||
#!/bin/sh | |||
### BEGIN INIT INFO | |||
# Provides: trafficmon | |||
# Required-Start: $local_fs | |||
# Required-Stop: $local_fs | |||
# X-Start-Before: $syslog | |||
# X-Stop-After: $syslog | |||
# Default-Start: 2 3 4 5 | |||
# Default-Stop: 0 1 6 | |||
# Short-Description: Monitor and record net traffic | |||
# Description: This service listens to a pipe for "iptables LOG" and | |||
# dnsmasq DNS query messages. It then records information | |||
# about internet access into a MySQL DB. Typically these | |||
# messages are provided by syslogd. | |||
# | |||
# NOTE: if this is started after syslogd the pipe might not | |||
# be available and syslogd will simply not try to write to | |||
# until a restart. | |||
### END INIT INFO | |||
NAME="trafficmon" | |||
DAEMON="/usr/sbin/$NAME" | |||
RUN=/run | |||
CONF="" | |||
SOCK="" | |||
# Pull in config | |||
if [ -r "/etc/default/$NAME" ]; then | |||
. /etc/default/$NAME | |||
fi | |||
### Setup control variables ### | |||
# This is where we put PID files and the pipe | |||
RUN="$RUN/poorman-ids" | |||
PID="$RUN/$NAME.pid" | |||
mkdir -p "$RUN" | |||
[ -n "$SOCK" ] || SOCK="$RUN/$NAME.sock" | |||
### ACTIONS ### | |||
# The main service command | |||
CTRL() { | |||
start-stop-daemon --pidfile "$PID" --exec "$DAEMON" "$@" | |||
} | |||
do_start() { | |||
echo -n "Starting Traffic Monitor: " | |||
if [ -z "$CONF" ]; then | |||
echo "NOT CONFIGURED" | |||
return 0 | |||
fi | |||
if CTRL --start --oknodo -- -c "$CONF" -b -i "$PID" -p "$SOCK"; then | |||
echo "OK" | |||
return 0 #JIC | |||
else | |||
echo "FAIL" | |||
return 1 | |||
fi | |||
} | |||
do_stop() { | |||
echo -n "Stoping Traffic Monitor: " | |||
if CTRL --stop --remove-pidfile; then | |||
echo "OK" | |||
return 0 #JIC | |||
else | |||
echo "FAIL" | |||
return 1 | |||
fi | |||
} | |||
do_status() { | |||
echo -n "Traffic Monitor is: " | |||
if CTRL --status; then | |||
echo "Up" | |||
return 0 #JIC | |||
else | |||
echo "Down" | |||
return 1 | |||
fi | |||
} | |||
### Main() | |||
case "$1" in | |||
start) | |||
do_start | |||
;; | |||
stop) | |||
do_stop | |||
;; | |||
restart) | |||
do_status && do_stop | |||
do_start | |||
;; | |||
status) | |||
do_status | |||
;; | |||
*) | |||
echo "$0 {start | stop | restart | status}" | |||
;; | |||
esac |
@@ -0,0 +1 @@ | |||
kern,daemon.* |/run/poorman-ids/trafficmon.sock |
@@ -0,0 +1,332 @@ | |||
////////////////////////////////////////////////////////////////////// | |||
// IP traffic monitor | |||
// Written by Jonathan A. Foster <ChipMaster@YeOlPiShack.net> | |||
// Started August 11th, 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. This will be read in real time from SysLogD | |||
// using the "pipe" feature. Access records will be logged to MySQL | |||
// for reporting and record keeping. | |||
////////////////////////////////////////////////////////////////////// | |||
// TODO: catch signals and attempt removal of PID file during shutdown. | |||
#include <string.h> | |||
#include <errno.h> | |||
#include <sys/types.h> | |||
#include <sys/stat.h> | |||
#include <syslog.h> | |||
#include <unistd.h> | |||
#include <string> | |||
#include <iostream> | |||
#include <fstream> | |||
#include <stdexcept> | |||
#include <vector> | |||
#include <map> | |||
#include "../cli.h" | |||
#include "../data.h" | |||
#include "../config.h" | |||
#include "appbase.h" | |||
using namespace std; | |||
////////////////////////////////////////////////////////////////////// | |||
// Monitor Config | |||
////////////////////////////////////////////////////////////////////// | |||
struct MonitorConf: public MonitorBaseConf { | |||
INIusList us; | |||
MonitorConf() { groups["us"] = &us; } | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Application class to store data passed in through a pipe or | |||
// file(s). | |||
////////////////////////////////////////////////////////////////////// | |||
//#define DEBUG | |||
struct TrafficMon: public TrafficMonBaseApp { | |||
LogAnalyzer analyze; | |||
istream *log; | |||
LiveBug bug; | |||
bool setup_db; | |||
bool piping; // are we sucking on a pipe? | |||
bool background_me; // daemonize? | |||
string pid_file; // Name of file to write PID in | |||
int inp_ct; // How many inputs were on the CLI | |||
bool running; // Whether or not its time to run logs | |||
long long line_no; // current line # in the active input | |||
TrafficMon(): | |||
log( 0), | |||
setup_db( false), | |||
piping( false), | |||
background_me(false), | |||
inp_ct( 0), | |||
running( false), | |||
line_no( 0) | |||
{ | |||
config = new MonitorConf; | |||
analyze.us = &(((MonitorConf *)config)->us.vals); | |||
} | |||
int help() { | |||
cerr << | |||
"\n" | |||
"trafficmon -c {config file} [-i] [-d] [-b] [-p {pipe name} | [{log name} ...]]\n" | |||
"\n" | |||
"Arguments:\n" | |||
" -c Load configuration from file (required)\n" | |||
" -i PID file to write (optional, use with -b)\n" | |||
" -d Create data tables (optional, use once)\n" | |||
" -b Run in background - daemonize (optional, use -i too)\n" | |||
" -p read from pipe creating it if needed (optional, good with -b)\n" | |||
"\n" | |||
"Other arguments are log files to be processed. This can be used to bulk\n" | |||
"load data prior to going live with an always on daemon or for catching\n" | |||
"up if the daemon stopped for some reason."; | |||
return ExitCode = 1; | |||
} | |||
unsigned do_switch(const char *sw) { | |||
if(sw[1]==0) { | |||
switch(*sw) { | |||
case 'b': background_me = true; return 0; | |||
case 'd': setup_db = true; return 0; | |||
case 'i': return 1; | |||
case 'p': piping = true; return !running; // second pass treats it as a file | |||
} | |||
} | |||
return TrafficMonBaseApp::do_switch(sw); | |||
} | |||
void do_switch_arg(const char *sw, const string &val) { | |||
// If "running" is set then we've already done this. So skip it! | |||
if(running) return; | |||
if(sw[1]==0) switch(*sw) { | |||
/// PID file to write ID in if we're daemonizing /// | |||
case 'i': | |||
pid_file = val; | |||
return; | |||
/// Make pipe if requested /// | |||
case 'p': | |||
// This can't wait for "running" since we want it to fail in foreground | |||
// Make read/write by process owner (root) only | |||
if(mkfifo(val.c_str(), 0600)<0 && errno!=17 /*already exists*/) { | |||
ExitCode = 2; | |||
throw runtime_error("Making pipe raised error "+str(errno)); | |||
} | |||
inp_ct++; | |||
} | |||
TrafficMonBaseApp::do_switch_arg(sw, val); | |||
} | |||
void do_arg(const char *fname) { | |||
// if not "running" we don't want to do this yet. Just count inputs. | |||
if(!running) { | |||
inp_ct++; | |||
return; | |||
} | |||
// the only thing we expect on the CLI is a log/pipe name | |||
if(log==&cin) log = 0; // won't happen right now | |||
if(log) | |||
((ifstream*)log)->close(); | |||
else | |||
log = new ifstream; | |||
if(!background_me) cerr << fname << ":\n"; | |||
restart: | |||
((ifstream*)log)->open(fname); | |||
ExitCode = run(); | |||
if(piping) { | |||
// If a process closes the write end of the pipe, like during log | |||
// rotation, we receive an EOF, end up here, and have to re-open the pipe | |||
// to be able to receive more on it. | |||
((ifstream*)log)->close(); | |||
sleep(1); // This is just to make sure we don't hog CPU in a loop | |||
goto restart; | |||
} | |||
} | |||
// This is the actual data process | |||
// probably should daemonize around here. | |||
int run() { | |||
string l; | |||
// prepare insert statement | |||
cppdb::statement db_add = db << | |||
"INSERT INTO connections " | |||
"(us, us_port, them, them_port, them_name, protocol, inbound) " | |||
"VALUES (?,?,?,?,?,?,?)"; | |||
/// parse log file /// | |||
line_no=0; | |||
while(getline(*log, l)) { | |||
line_no++; | |||
if(!background_me) | |||
cerr << bug << ' ' << line_no << '\r' << flush; | |||
/// process connections /// | |||
if(analyze.line(l)) { | |||
// TODO: time stamp handling? | |||
// insert record | |||
db_add.reset(); | |||
db_add << analyze.conn.us << analyze.conn.us_port << analyze.conn.them | |||
<< analyze.conn.them_port << analyze.conn.name | |||
<< analyze.conn.protocol << analyze.conn.in | |||
<< cppdb::exec; | |||
} | |||
} | |||
// In a real pipe situation this should never get reached. | |||
if(!background_me) { | |||
cerr << "\nLines: " << line_no | |||
<< "\nTotal rDNS: " << analyze.rdns.size() << '\n' << endl; | |||
} | |||
return 0; | |||
} | |||
void create_tables() { | |||
/// Connection recrods /// | |||
db << "CREATE TABLE connections (" | |||
"id INT NOT NULL AUTO_INCREMENT PRIMARY KEY," | |||
"tstamp TIMESTAMP NOT NULL," // default is NOW() | |||
"us CHAR(80) NOT NULL DEFAULT ''," | |||
"us_port INT NOT NULL DEFAULT 0," | |||
"them CHAR(80) NOT NULL DEFAULT ''," | |||
"them_port INT NOT NULL DEFAULT 0," | |||
"them_name CHAR(128) NOT NULL DEFAULT ''," | |||
"protocol CHAR(12) NOT NULL DEFAULT ''," | |||
"inbound TINYINT(1) NOT NULL DEFAULT 0," | |||
"INDEX tstamp(tstamp, them_name)" | |||
") Engine=MyISAM" | |||
<< cppdb::exec; | |||
// NOTE: MyISAM is thousands of times faster than InnoDB for this sort | |||
// of work load. Mostly continuous writes with occasional reads. | |||
// But will it work for all I'm going to do? AriaDB is slower but | |||
// significantly faster than InnoDB. | |||
/// Status of domain names /// | |||
db << "CREATE TABLE dns (" | |||
"name CHAR(128) NOT NULL PRIMARY KEY," | |||
"decided TIMESTAMP NOT NULL," | |||
"status TINYINT(1) NOT NULL DEFAULT 0," | |||
"note CHAR(128) NOT NULL DEFAULT ''" | |||
") Engine=MyISAM" | |||
<< cppdb::exec; | |||
/// wild card DNS list /// | |||
// NOTE: All of these names are treated as if prefixed with "*.". At this | |||
// time there are only plans to implement this as a black list. | |||
// Many domains are sevices with threat potential and discovering | |||
// all of the host and subdomains may not be viable. Think ad and | |||
// tracking services. | |||
db << "CREATE TABLE dns_wild (" | |||
"name CHAR(128) NOT NULL PRIMARY KEY," | |||
"decided TIMESTAMP NOT NULL," | |||
"status TINYINT(1) NOT NULL DEFAULT 2," | |||
"note CHAR(128) NOT NULL DEFAULT ''" | |||
") Engine=MyISAM" | |||
<< cppdb::exec; | |||
} | |||
int main() { | |||
int x; | |||
try { | |||
if(x=TrafficMonBaseApp::main()) return x; | |||
if(!((MonitorConf*)config)->us.vals.size()) throw CLIerror( | |||
"The configuration files MUST contain an [us] section with " | |||
"appropriate values" | |||
); | |||
if(!config->traffic_mon.vals.size()) throw CLIerror( | |||
"The configuration files MUST contain an [Traffic Mon] section with " | |||
"appropriate values" | |||
); | |||
if(piping && inp_ct!=1) throw CLIerror( | |||
"Pipe requires one and only one file name to read from." | |||
); | |||
if(setup_db) create_tables(); | |||
// if requested attempt daemonizing. | |||
if(background_me) { | |||
if(inp_ct==0) throw CLIerror( | |||
"Backgrounding requires the specification of an input source. " | |||
"STDIN is unavailable when we move to background." | |||
); | |||
if(daemon(0,0)<0) throw CLIerror("Failed to switch to background"); | |||
// If we get here we're in the background. No STDIN, STDOUT, STDERR | |||
if(pid_file!="") { // pid_file requested | |||
ofstream pf(pid_file.c_str()); | |||
pf << getpid(); | |||
} | |||
} | |||
} catch(const CLIerror &e) { | |||
cerr << "ERROR: " << e.what() << "\n"; | |||
help(); | |||
return ExitCode ? ExitCode : 1; // JIC someone sets a different exit code | |||
} | |||
try { | |||
// CLI processed now lets analyze data. | |||
running = true; | |||
// TODO: this is going to break things? Probably should prevent reloading conf. DB is opened in TrafficMonBaseApp::main() | |||
// Actually I think it may actualy work... | |||
cBaseApp::main(); // re-run CLI args for inputs | |||
if(!log) { | |||
// no inputs spec'd on CLI assume stdin (we're not a daemon) | |||
log = &cin; | |||
ExitCode = run(); | |||
} | |||
} catch(const exception &e) { | |||
if(ExitCode==0) ExitCode = 2; | |||
if(background_me) { | |||
openlog("trafficmon", LOG_CONS | LOG_PID, LOG_DAEMON); | |||
syslog(LOG_ALERT, "%s", e.what()); | |||
closelog(); | |||
} else | |||
cerr << "ERROR: " << e.what() << "\n"; | |||
} | |||
return ExitCode; | |||
} | |||
}; | |||
////////////////////////////////////////////////////////////////////// | |||
// Run it | |||
////////////////////////////////////////////////////////////////////// | |||
MAIN(TrafficMon) |