9 Commits

Author SHA1 Message Date
  Jon Foster ddca2693c4 Adapt CNAME handler to handle more CNAMEs 2 years ago
  Jon Foster d12a3e9f4d Traffic monitor fixes and block exports 2 years ago
  Jon Foster bb0278f2a5 Control Panel fixes & whole domain blocks 2 years ago
  Jon Foster 9ca68a65f5 CNAME handling 2 years ago
  Jon Foster 2d57afcacd Minor package improvments 3 years ago
  Jon Foster ffab54d30d controlpanel: URL handling & debugging 3 years ago
  Jon Foster cd7d62ca05 Begin realtime tracking 3 years ago
  Jon Foster 2876b4125a Import MiniINI and refactor config.o 3 years ago
  Jon Foster 8a33f5fdd8 Break analyzer out of iptraffic & tests 3 years ago
39 changed files with 2745 additions and 124 deletions
Split View
  1. +11
    -0
      .gitignore
  2. +46
    -14
      Makefile
  3. +17
    -0
      TODO
  4. +15
    -0
      cli.cpp
  5. +11
    -1
      cli.h
  6. +39
    -40
      config.cpp
  7. +38
    -5
      config.h
  8. +21
    -0
      controlpanel/Makefile
  9. +12
    -0
      controlpanel/TODO.md
  10. +37
    -0
      controlpanel/data.h
  11. +12
    -0
      controlpanel/default
  12. +117
    -0
      controlpanel/init
  13. +28
    -0
      controlpanel/lighttpd.conf
  14. +114
    -0
      controlpanel/mainskin.tmpl
  15. +68
    -0
      controlpanel/sample.js
  16. +266
    -0
      controlpanel/trafficctrl.cpp
  17. +109
    -0
      data.cpp
  18. +22
    -0
      data.h
  19. +22
    -60
      iptraffic.cpp
  20. +129
    -0
      miniini.cpp
  21. +136
    -0
      miniini.h
  22. +128
    -0
      poorman-ids.dpak
  23. +32
    -0
      sample.conf
  24. +39
    -1
      strutil.cpp
  25. +13
    -3
      strutil.h
  26. +1
    -0
      tests/.gitignore
  27. +8
    -0
      tests/Makefile
  28. +81
    -0
      tests/data.cpp
  29. +52
    -0
      tests/testit.cpp
  30. +51
    -0
      tests/testit.h
  31. +113
    -0
      trafficmon/appbase.cpp
  32. +85
    -0
      trafficmon/appbase.h
  33. +209
    -0
      trafficmon/badtrafficrpt.cpp
  34. +10
    -0
      trafficmon/default
  35. +139
    -0
      trafficmon/dnsblacklist.cpp
  36. +61
    -0
      trafficmon/domblacklist.cpp
  37. +120
    -0
      trafficmon/init
  38. +1
    -0
      trafficmon/syslog
  39. +332
    -0
      trafficmon/trafficmon.cpp

+ 11
- 0
.gitignore View File

@@ -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/


+ 46
- 14
Makefile View File

@@ -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

+ 17
- 0
TODO View File

@@ -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.

+ 15
- 0
cli.cpp View File

@@ -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.


+ 11
- 1
cli.h View File

@@ -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


+ 39
- 40
config.cpp View File

@@ -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;
}

+ 38
- 5
config.h View File

@@ -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

+ 21
- 0
controlpanel/Makefile View File

@@ -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

+ 12
- 0
controlpanel/TODO.md View File

@@ -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

+ 37
- 0
controlpanel/data.h View File

@@ -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

+ 12
- 0
controlpanel/default View File

@@ -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

+ 117
- 0
controlpanel/init View File

@@ -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

+ 28
- 0
controlpanel/lighttpd.conf View File

@@ -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.

+ 114
- 0
controlpanel/mainskin.tmpl View File

@@ -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 %>

+ 68
- 0
controlpanel/sample.js View File

@@ -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"
}
}
}

+ 266
- 0
controlpanel/trafficctrl.cpp View File

@@ -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;
}


+ 109
- 0
data.cpp View File

@@ -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;
}

+ 22
- 0
data.h View File

@@ -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

+ 22
- 60
iptraffic.cpp View File

@@ -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;
}



+ 129
- 0
miniini.cpp View File

@@ -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';
}
}

+ 136
- 0
miniini.h View File

@@ -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

+ 128
- 0
poorman-ids.dpak View File

@@ -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

+ 32
- 0
sample.conf View File

@@ -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

+ 39
- 1
strutil.cpp View File

@@ -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
//////////////////////////////////////////////////////////////////////



+ 13
- 3
strutil.h View File

@@ -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; }
};


+ 1
- 0
tests/.gitignore View File

@@ -0,0 +1 @@
/data

+ 8
- 0
tests/Makefile View File

@@ -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

+ 81
- 0
tests/data.cpp View File

@@ -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();
}

+ 52
- 0
tests/testit.cpp View File

@@ -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;
}

+ 51
- 0
tests/testit.h View File

@@ -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";

}

+ 113
- 0
trafficmon/appbase.cpp View File

@@ -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;
}

+ 85
- 0
trafficmon/appbase.h View File

@@ -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

+ 209
- 0
trafficmon/badtrafficrpt.cpp View File

@@ -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)

+ 10
- 0
trafficmon/default View File

@@ -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

+ 139
- 0
trafficmon/dnsblacklist.cpp View File

@@ -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)

+ 61
- 0
trafficmon/domblacklist.cpp View File

@@ -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)

+ 120
- 0
trafficmon/init View File

@@ -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

+ 1
- 0
trafficmon/syslog View File

@@ -0,0 +1 @@
kern,daemon.* |/run/poorman-ids/trafficmon.sock

+ 332
- 0
trafficmon/trafficmon.cpp View File

@@ -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)

Loading…
Cancel
Save