]> gothamcode git trees - phailomatic.git/commitdiff
Initial check-in
authorGitolite Admin <hostmaster@vnetworx.net>
Sun, 9 Feb 2014 06:41:55 +0000 (01:41 -0500)
committerGitolite Admin <hostmaster@vnetworx.net>
Sun, 9 Feb 2014 06:41:55 +0000 (01:41 -0500)
CHANGELOG [new file with mode: 0644]
README [new file with mode: 0644]
phailomatic [new file with mode: 0755]
phailomatic.1.gz [new file with mode: 0644]
phailomatic.conf.sample [new file with mode: 0644]

diff --git a/CHANGELOG b/CHANGELOG
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..7559c80
--- /dev/null
+++ b/README
@@ -0,0 +1,37 @@
+Phailomatic
+===========
+Phailomatic is a relatively lightweight intrusion prevention system written
+in PHP.  It is intended for use on embedded devices with PHP present for some
+reason other than running Phailomatic.  Phailomatic's home is at:
+http://gothamcode.com/phailomatic
+
+Phailomatic will monitor up to 5 log files, in a single-threaded manner.
+
+Command-line options:
+=====================
+--help (displays minimal help)
+--cleanup (cleans up firewall configuration changes)
+
+Configuring Phailomatic:
+========================
+Phailomatic can read configuration data from the commandline, but this is very
+ugly.  You are advised to perform all configuration via a configuration file.
+A sample configuration file is provided as phailomatic.config.sample .
+
+The configuration file will be looked for in the following locations in the
+following order:
+
+1. Current directory
+2. /opt/etc/phailomatic.conf (Optware/Entware)
+3. /opt/etc/phailomatic/phailomatic.conf (Optware/Entware)
+4. /etc/phailomatic.conf
+5. /etc/phailomatic/phailomatic.conf
+
+STDIN and STDOUT can be re-opened and redirected somewhere after daemonization.
+
+The Phailomatic man page contains the full documentation.
+
+
+Help can be sought on the Phailomatic mailing list:
+http://lists.gothamcode.com/listinfo/phailomatic
+
diff --git a/phailomatic b/phailomatic
new file mode 100755 (executable)
index 0000000..6481bdf
--- /dev/null
@@ -0,0 +1,913 @@
+#!/usr/bin/env php
+<?php
+/*
+ * phailomatic :: Because sometimes Python isn't an option
+ *
+ * Version 1.0, February 6, 2014
+ * Copyright (c) 2014, Ron Guerin <ron@vnetworx.net>
+ *
+ * This script implements an intrusion prevention/denial of service mitigation
+ * system in PHP.
+ *
+ * PHP Extentions: Requires: PCRE.  Optional: PCNTL, POSIX
+ *
+ * Do not edit this script!  Edit /etc/${scriptname}.conf to change settings.
+ * where ${scriptname} is the name of this file.  Changes made to the script
+ * will get overwritten on upgrades.
+ *
+ * Phailomatic is Free Software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Phailomatic is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * If you are not able to view the file COPYING, please write to the
+ * Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ * to get a copy of the GNU General Public License or to report a
+ * possible license violation.
+ *
+ * @package Phailomatic
+ * @author Ron Guerin <ron@vnetworx.net>
+ * @license http://www.fsf.org/licenses/gpl.html GNU Public License
+ * @copyright Copyright &copy; 2014 Ron Guerin
+ * @filesource
+ * @link http://gothamcode.com/phailomatic Phailomatic
+ * @version 1.0
+ *
+ */
+
+// Configuration variables:
+// pidpath, hostname, checkdelay, pregex, firewall, teardown, ban, unban,
+// checkdelay, debug, daemonize, silentmax, trigger[include], trigger[exec],
+// pregex, firewall[], teardown[], ban, unban, stdout, stderr, file1, file2, ...,
+//
+// To change, edit /etc/${scriptname}.conf .
+
+// Regexes must match as a named capture the IP address of the connecting client,
+// and begin following the syslog date and severity.  For example in the line:
+// Feb  6 00:30:25 localhost mail.info postfix/smtpd[23527]: ...
+// regexes should start matching with "postfix" and continue to the end of the line.
+// Please note that Phailomatic *requires* you to match to the end of the line,
+// but DO NOT include the end of line marker ($) in your regex.
+//
+// Firewall command substitutions: $1 = port number, $2 = IP address
+// Each firewall/teardown command will be executed in order
+// firewall[1]=iptables -N phailomatic-$1
+// firewall[2]=iptables -A phailomatic-$1 -j RETURN
+// firewall[3]=iptables -I INPUT -p tcp --dport $1 -j phailomatic-$1
+//
+// Triggers: Phailomatic can either call a PHP function or execute a commandline or both.
+// trigger[include]=/path/to/yourcode.php
+// This file will be included into Phailomatic and must contain:
+// function phailtrigger($ip, $ports) // IP and array of port numbers will be passed to this function when something is blocked
+//
+// trigger[exec]=/path/to/trigger $2 $1
+// when we execute this commandline, $2 will be replaced by IP, $1 by string of ports separated by commas
+//
+// Each file can contain multiple regex definitions.
+//
+// file1[path]=/var/log/messages
+// file1[syslogformat]=TRUE
+// file1[regex][1][pattern]=dropbear\[[0-9]+\]: (?:bad password|login) attempt for .* from ::ffff:(?P<ip>[0-9.]+):[0-9]+
+// file1[regex][1][ports]=22
+// file1[regex][1][hitperiod]=90
+// file1[regex][1][timeout]=600
+// file1[regex][1][bantrigger]=5
+// file1[regex][2][pattern]=postfix/smtpd\[[0-9]+\]: .*\[(?P<ip>[0-9.]+)\]: SASL (?:[A-Z]+) authentication failed: authentication failure
+// file1[regex][2][ports]=25,587,2525
+
+define('VERSION', '1.0.0');
+define('LOG_PRIO', LOG_AUTH|LOG_INFO);
+define('ERROR', 1);
+define('CANT_OPEN_CONF', 2);
+define('ALREADY_RUNNING', 3);
+define('BAD_CONFIG', 100);
+define('BAD_CONFIG_DIRS', 101);
+if (! defined('SIGKILL')) {
+       define('SIGHUP', 1);
+       define('SIGALRM', 14);
+       define('SIGTERM', 15);
+       define('SIGUSR1', 16);
+       define('SIGUSR2', 17);
+       define('SIGCHLD', 17);
+       define('SIGTSTP', 20);
+       define('SIGTTIN', 21);
+       define('SIGTTOU', 22);
+       define('SIGCONT', 25);
+}
+define('MONITOR_MAX', 5); // Max number of log files to monitor, recommend not to increase this
+
+@date_default_timezone_set(@date_default_timezone_get()); // Sadly, this is necessary
+
+$myname = preg_replace(chr(7).'.php$'.chr(7), '', basename($argv[0])); // Get my name, minus any .php extension
+openlog($myname, LOG_PID|LOG_PERROR, LOG_AUTH);
+
+// Enable garbage collection (this means PHP 5.3+ now)
+if (function_exists('gc_enable')) gc_enable();  // Collect garbage every 60 cycles below
+
+// Settings
+$confvars = array('debug' => NULL, 'pidpath' => '/var/run', 'hostname' => php_uname('n'), 'myname' => $myname,
+       'ignoresigs' => array(SIGCHLD, SIGTSTP, SIGTTOU, SIGTTIN), 'daemonize' => TRUE,
+       'help' => NULL, 'cleanup' => NULL,
+       'checkdelay' => NULL, 'silentmax' => NULL, 'trigger' => NULL, 'pregex' => NULL,
+       'firewall' => NULL, 'ban' => NULL, 'unban' => NULL, 'teardown' => NULL,
+       'mustconf' => TRUE, 'stdin' => NULL, 'stdout' => NULL, 'stderr' => NULL, 'error' => array());
+for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+       $confvars['file'.$i] = NULL;
+       ${'iptable'.$i} = array(); // $iptable1, $iptable2, ...
+}
+
+$config = init_settings($confvars, $argv); // Get settings from defaults, config file, and commandline
+if ($config === FALSE) {
+       if ($confvars['error'][0] == CANT_OPEN_CONF) {
+               $retcode = CANT_OPEN_CONF;
+               log_msg(LOG_PRIO, $confvars, 'Error: '.((array_key_exists('myconfigname', $confvars) && $confvars['myconfigname'])
+                       ? $confvars['myconfigname'] : $confvars['myname']).'.conf not found. Can\'t continue. Terminating', TRUE);
+       }
+       else {
+               foreach ($confvars['error'] as $msg) log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+               $retcode = BAD_CONFIG;
+       }
+       closelog();
+       die((int) $retcode);
+}
+
+if (isset($confvars['trigger']['include']) && ($confvars['trigger']['include'] != NULL))
+       include($confvars['trigger']['include']); // include triggercode
+
+foreach ($argv as $arg) { // Check args for --help, print if requested and exit
+       $arg = strtolower($arg);
+       if (($arg == '--help') || ($arg == '-h')) {
+               echo "Phailomatic is an intrusion-prevention/denial-of-service-mitigation daemon.\n\n";
+               echo "Options: --cleanup  (cleans up firewall changes)\n";
+               echo "All other settings are expected to be specified in a configuration file.\n";
+               echo "\nPlease see the man page ".$confvars['myname']."(1) for more information.\n";
+               exit;
+       }
+       elseif ($arg == '--cleanup') {
+               $msg = 'Cleaning up firewall changes.';
+               log_msg(LOG_PRIO, $confvars, $msg);
+               firewall_configure($confvars, FALSE);
+               closelog();
+               exit;
+       }
+}
+
+// Daemonize and return lock (or just lock), handle TERM and HUP signals
+// Note to self: Open any database after forking (daemonization) to avoid pain & suffering
+if ($confvars['daemonize'] && function_exists('pcntl_fork') && function_exists('posix_setsid')) {
+       $lock = proc_daemonize($confvars['myname'], $error, $confvars['pidpath'], $confvars['ignoresigs'], $confvars['stdin'], $confvars['stdout'], $confvars['stderr']);
+       if ($lock === FALSE) {
+               switch ($error) {
+                       case ALREADY_RUNNING:
+                               $msg = $confvars['myname'].' already running.';
+                               break;
+                       case CANT_SETSID:
+                               $msg = $confvars['myname'].' could not setsid.';
+                               break;
+               }
+               log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+               die((int) $error);
+       }
+       pcntl_signal(SIGTERM, 'sig_handler');
+       pcntl_signal(SIGHUP,  'sig_handler');
+}
+else {
+       $confvars['stdin'] = STDIN; $confvars['stdout'] = STDOUT; $confvars['stderr'] = STDERR;
+       // Attempt to get exclusive lock on pidfile
+       $lock = fopen($confvars['pidpath'].'/'.$confvars['myname'].'.pid', 'c+');
+       if (! flock($lock, LOCK_EX | LOCK_NB)) {
+           log_msg(LOG_PRIO, $confvars, $confvars['myname'].' already running.', TRUE);
+               die((int) ALREADY_RUNNING);
+       }
+       else {
+               // Write PID to pidfile
+               fseek($lock, 0);
+               ftruncate($lock, 0);
+               fwrite($lock, getmypid());
+               fflush($lock);
+       }
+}
+
+// Now get about the point of all this...
+$cycle = 0; $debugcycle = 0; $somethingtomonitor = FALSE;
+
+for ($i=1; $i<(MONITOR_MAX+1); ++$i) if ($confvars['file'.$i] != NULL) $somethingtomonitor = TRUE;
+if (! $somethingtomonitor) {
+       $msg = 'Not configured to watch any logfiles, shutting down.';
+       log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+       closelog();
+       die($msg."\n");
+}
+
+log_msg(LOG_PRIO, $confvars, 'Phailomatic version '.VERSION.' starting, watching logs.');
+firewall_configure($confvars);
+while (TRUE) {
+       for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+               if ($confvars['file'.$i] == NULL) continue;
+               if (! isset($confvars['file'.$i]['waiting'])) $confvars['file'.$i]['waiting'] = 0;
+               $data = logfile_read($confvars['file'.$i]['path'], $confvars['file'.$i]['position']);
+               if ($data === FALSE) {
+                       $confvars['file'.$i]['waiting'] = $confvars['file'.$i]['waiting'] + $confvars['checkdelay'];
+                       if ($confvars['file'.$i]['waiting'] > $confvars['silentmax']) {
+                               $msg = 'WARNING: Operation suspended for '.$confvars['file'.$i]['path'].' which hasn\'t existed for at least '.$confvars['silentmax'].' seconds.';
+                               log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+                       }
+               }
+               else {
+                       if ($confvars['file'.$i]['waiting'] > $confvars['silentmax']) {
+                               $msg = 'NOTICE: '.$confvars['file'.$i].' finally exists.  Resuming normal operation.';
+                               log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+                       }
+                       $confvars['file'.$i]['waiting'] = 0;
+                       $fails = logfile_process_data($confvars, $data, $i);
+                       firewall_evaluate($confvars, $fails, ${'iptable'.$i});
+               }
+               signal_dispatch();
+       }
+
+       if ($cycle & 1) { // expire every odd numbered cycle
+               for ($i=1; $i<(MONITOR_MAX+1); ++$i) if ($confvars['file'.$i] != NULL) firewall_ban_expirer($confvars, ${'iptable'.$i});
+       }
+
+       if (++$cycle == 60) {
+               $cycle = 0;
+               if (function_exists('gc_collect_cycles')) gc_collect_cycles(); // collect garbage every 60 cycles
+       }
+
+       sleep ($confvars['checkdelay']);
+}
+// We can never get here because the while loop never ends
+
+/* *********************************************************************** */
+
+/**
+ * Reads log data from file, based on position.
+ *
+ * @package phailomatic
+ * @param string $file file to read .
+ * @param int &$position starting position
+ * @return bool $forward whether to start reading the file
+ *  at the beginning or the end.
+ */
+function logfile_read($file, &$position) {
+       $data = ''; clearstatcache();
+       if (! file_exists($file)) return FALSE;
+       $cursize = filesize($file);
+       if ($cursize < $position) $position = 0;
+       if ($cursize != $position) {
+               $fh = fopen($file, 'rb');
+               fseek($fh, ($position - $cursize) - 1, SEEK_END);
+               while ((! feof($fh)) && ($position > 0)) $data .= fgets($fh, 1024);
+               fclose($fh);
+               $position = $cursize;
+       }
+       return $data;
+}
+
+/**
+ * Process log data place restrictions
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param string $data data to process from logfile read.
+ * @param int $i file number
+ * @return array $block array of ip/port combinations that should be blocked
+ */
+function logfile_process_data($confvars, $data, $fileno) {
+       $block = array();
+
+       if ($confvars['file'.$fileno]['syslogformat']) $pre = $confvars['pregex'];
+       else $pre = '';
+       foreach ($confvars['file'.$fileno]['regex'] as $key => $rule) {
+               // Look for all matches in this chunk of data
+               if (preg_match_all(chr(7).'^'.$pre.$rule['pattern'].'$'.chr(7).'m', $data, $matches) != FALSE) {
+                       // Loop through matches
+                       foreach ($matches['ip'] as $ip) {
+                               $block[$ip]['count'] = isset($block[$ip]['count']) ? ++$block[$ip]['count'] : 1;
+                               $block[$ip]['hitperiod'] =  $rule['hitperiod'];
+                               $block[$ip]['bantrigger'] = $rule['bantrigger'];
+                               $block[$ip]['ports'] = isset($block[$ip]['ports']) ? $block[$ip]['ports'] + $rule['ports'] : $rule['ports'];
+                               $block[$ip]['timeout'] =  $rule['timeout'];
+                       }
+               }
+       }
+       return $block;
+}
+
+/**
+ * Sends configuration commands to either set up or tear down firewall changes
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param bool $state TRUE (default) to set up, or FALSE to tear down
+ */
+function firewall_configure($confvars, $state=TRUE) {
+       if ($state == TRUE) $which = 'firewall';
+       else $which = 'teardown';
+
+       $blockport = array();
+       for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+               if ($confvars['file'.$i] == NULL) continue;
+               // this shakes out duplicate ports
+               foreach ($confvars['file'.$i]['regex'] as $rule) foreach ($rule['ports'] as $port) $blockport[$port] = TRUE;
+       }
+
+       foreach ($blockport as $port => $x) {
+               foreach ($confvars[$which] as $fwrule) {
+                       exec(str_replace('$1', $port, $fwrule), $out);
+                       if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+               }
+       }
+}
+
+/**
+ * Restrict access by address and port via the system firewall
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param array $fails array addresses and ports that failed authentication
+ * @param array $iptable table of restricted IPs
+ */
+function firewall_evaluate($confvars, $fails, &$iptable) {
+
+       foreach ($fails as $ip => $rule) {
+               // If count not set or expired without triggering a ban, set count to 0
+               if ((! isset($iptable[$ip]['count'])) ||
+                       (($iptable[$ip]['expires'] <= date('U')) && (($iptable[$ip]['count'] + $rule['count']) < $rule['bantrigger'])))
+                               $iptable[$ip]['count'] = 0;
+               $iptable[$ip]['expires'] = date('U') + $rule['hitperiod'];
+               $oldcount = $iptable[$ip]['count'];
+               $iptable[$ip]['count'] = $iptable[$ip]['count'] + $rule['count'];
+               if (($oldcount <  $rule['bantrigger']) && ($iptable[$ip]['count'] >= $rule['bantrigger'])) {
+                       $s = (count($rule['ports']) > 1) ? 's' : '';
+                       $msg = 'Banning '.$ip.' on port'.$s.': '.implode(' ',$rule['ports']);
+                       log_msg(LOG_PRIO, $confvars, $msg);
+                       //echo date('M j h:i:s ').'Banning '.$ip.' on port'.$s.': '.implode(' ', $rule['ports']).' '.date('r', $authtime)."\n";
+                       $base = str_replace('$2', $ip, $confvars['ban']);
+                       foreach ($rule['ports'] as $port) {
+                               exec(str_replace('$1', $port, $base), $out);
+                               if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+                       }
+                       $iptable[$ip]['ports'] = isset($iptable[$ip]['ports']) ? $iptable[$ip]['ports'] + $rule['ports'] : $rule['ports'];
+
+                       $iptable[$ip]['expires'] = date('U') + $rule['timeout'];
+                       if (function_exists('phailtrigger')) phailtrigger($ip, $rule['ports']);
+
+                       if (isset($confvars['trigger']['exec']) && ($confvars['trigger']['exec'] != NULL))
+                               exec(str_replace(array('$2', '$1'), array($ip, implode(',',$rule['ports'])), $confvars['trigger']['exec']), $out);
+               }
+       }
+}
+
+/**
+ * Un-restrict access by address and port via the system firewall
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param array $iptable table of restricted IPs
+ */
+function firewall_ban_expirer($confvars, &$iptable, $all=FALSE) {
+       // If something expired, undo the ban
+       if (! is_array($iptable)) return;
+       foreach ($iptable as $ip => $state) {
+               if ($all || (isset($state['expires']) && ($state['expires'] < date('U')))) {
+                       if (isset($state['ports'])) {
+                               $s = (count($state['ports']) > 1) ? 's' : '';
+                               $msg = 'Ending ban of '.$ip.' on port'.$s.': '.implode(' ',$state['ports']);
+                               log_msg(LOG_PRIO, $confvars, $msg);
+                               //echo date('M j h:i:s ').'Ending ban of '.$ip.' on port'.$s.': '.implode(' ',$state['ports'])."\n";
+                               $base = str_replace('$2', $ip, $confvars['unban']);
+                               foreach ($state['ports'] as $port) {
+                                       exec(str_replace('$1', $port, $base), $out);
+                                       if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+                               }
+                       }
+                       unset($iptable[$ip]);
+               }
+       }
+}
+
+/**
+ * Flush live IP data, undo bans
+ *
+ * @package phailomatic
+ * @param int $signo signal
+ */
+function flush_live_data() {
+       // This is ugly, ugly, ugly code  TODO: WTF globals
+       global $confvars;
+
+       for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+               if ($confvars['file'.$i] != NULL) {
+                       global ${'iptable'.$i};
+                       firewall_ban_expirer($confvars, ${'iptable'.$i}, TRUE);
+               }
+       }
+}
+
+/**
+ * Dispatch signals from PCNTL or pseudo-signals from filesystem
+ *
+ * @package phailomatic
+ * @param void
+ */
+function signal_dispatch() {
+       global $myname, $confvars;
+
+       if (function_exists('pcntl_signal_dispatch')) {
+               pcntl_signal_dispatch();
+               return;
+       }
+       elseif (file_exists('/var/run/'.$myname.'-sigterm')) {
+               unlink('/var/run/'.$myname.'-sigterm');
+               sig_handler(SIGTERM);
+       }
+       elseif (file_exists('/var/run/'.$myname.'-sighup')) {
+               unlink('/var/run/'.$myname.'-sighup');
+               sig_handler(SIGHUP);
+       }
+       elseif (file_exists('/var/run/'.$myname.'-sigusr1')) {
+               unlink('/var/run/'.$myname.'-sigusr1');
+               sig_handler(SIGUSR1);
+       }
+}
+
+/**
+ * Handle signals we might receive
+ *
+ * @package phailomatic
+ * @param int $signo signal
+ */
+function sig_handler($signo){
+       global $myname, $confvars, $argv;
+
+       switch ($signo) {
+               case SIGTERM:
+                       // handle shutdown tasks
+                       $msg = 'Clearing IPs and shutting down.';
+                       log_msg(LOG_PRIO, $confvars, $msg);
+                       firewall_configure($confvars, FALSE);
+                       closelog();
+                       exit;
+                       break;
+               case SIGHUP:
+                       // handle reload tasks
+                       $msg = 'Reloading configuration settings. This also flushes bans and fail counts.';
+                       log_msg(LOG_PRIO, $confvars, $msg);
+                       flush_live_data();
+                       init_settings($confvars, $argv);
+               case SIGUSR1:
+                       // flush current IP bans and fail counts
+                       $msg = 'Flushing list of banned IPs and fail counts.';
+                       log_msg(LOG_PRIO, $confvars, $msg);
+                       flush_live_data();
+                       break;
+       }
+}
+
+/**
+ * Find location of config file and return it
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @return mixed config file path or FALSE if could not locate config file
+ */
+function config_location($confvars) {
+       // Find config file, and return it.  Looks first in current directory,
+       // then iterates through array of standard directories.  For each
+       // standard directory, it first looks in the standard directory, then in
+       // whatever subdirectories are specified in array $confvars['mydirs'].
+       // Tries to detect Optware, searches /opt first when detected.
+
+       $myname = (isset($confvars['myconfigname'])) ? $confvars['myconfigname'] : $confvars['myname'];
+       $mydirs = (isset($confvars['mydirs'])) ? $confvars['mydirs'] : array($myname);
+       $conf = './'.$myname.'.conf';
+       // This is a crude test for Optware/Entware
+       $return['embedded'] = (file_exists('/opt/bin/ipkg-opt') || file_exists('/opt/bin/opkg')) ? TRUE : FALSE;
+       if (file_exists($conf)) {
+               $return['path'] = $conf;
+               return $return;
+       }
+       $prefixes = ($return['embedded']) ? array('/opt', '') : array('');
+       $confdirs = array_merge((array)'', $mydirs); // '' == current directory
+       $stddirs = array('/usr/local/etc', '/etc');
+
+       foreach ($prefixes as $prefix) {
+               foreach ($stddirs as $base) {
+                       foreach ($confdirs as $dir) {
+                               if ($dir != '') $dir = $dir.'/';
+                               $conf = $prefix.$base.'/'.$dir.$myname.'.conf';
+                               if (file_exists($conf)) {
+                                       $return['path'] = $conf;
+                                       return $return;
+                               }
+                       }
+               }
+       }
+       $return['path'] = FALSE;
+       return $return;
+}
+
+/**
+ * Initialize settings
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param mixed $argv command-line arguments or FALSE
+ * @return bool TRUE on success or FALSE
+ */
+function init_settings(&$confvars, $argv=FALSE) {
+       # TODO: if we configured plugins, load them so they can modify $confvars
+       // This function is generic and passes data via $confvars
+       $cmdline = TRUE; // Command line is either valid or not-applicable (web use)
+       $config = FALSE; // We haven't found a config file (yet)
+       $confvars['error'] = NULL; // We turn this into an array to pass data back
+
+       if ($argv) {
+               // Parse out any command-line args first, skipping argv[0]
+               $cmdlinevars = array();
+               $cmdlineargs = array();
+               $optsdone = FALSE;
+               $skip = TRUE;
+
+               foreach ($argv as $argnum => $arg) {
+                       if ($skip) $skip = FALSE;
+                       else {
+                               if ($arg == '--') $optsdone = TRUE;
+                               if ((substr($arg, 0, 2) == '--') && (! $optsdone)) {
+                                       $sep = strpos($arg,'=');
+                                       if ($sep !== FALSE) { // This is set to a value
+                                               $val = trim(substr($arg, $sep + 1));
+                                               $var = trim(substr($arg, 2, $sep - 2));
+                                               if (substr($var, -1, 1) == ']') {
+                                                               $array=array();
+                                                               $indexes = substr($var, strpos($var, '['));
+                                                               $var = substr($var, 0, strpos($var, '['));
+                                                               while (strpos($indexes, '[') !== FALSE) {
+                                                                               $b = strpos($indexes, ']');
+                                                                               $array[] = substr($indexes, 1, $b -1);
+                                                                               $indexes = substr($indexes, $b + 1);
+                                                               }
+                                               }
+                                       }
+                                       else { // This is an implicit setting of TRUE
+                                               $var = trim(substr($arg, 2));
+                                               $val = TRUE;
+                                       }
+
+                                       if (array_key_exists($var, $confvars)) {
+                                               if ($val == '') $val = NULL;
+                                               if (strtoupper($val) == 'NULL') $val = NULL;
+                                               if (strtoupper($val) == 'TRUE') $val = TRUE;
+                                               if (strtoupper($val) == 'FALSE') $val = FALSE;
+                                               if (isset($array)) {
+                                                       switch (count($array)) {
+                                                                       case 5:
+                                                                                       $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]][$array[3]][$array[4]] = $val;
+                                                                                       break;
+                                                                       case 4:
+                                                                                       $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]][$array[3]] = $val;
+                                                                                       break;
+                                                                       case 3:
+                                                                                       $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]] = $val;
+                                                                                       break;
+                                                                       case 2:
+                                                                                       $cmdlinevars[${$var}][$array[0]][$array[1]] = $val;
+                                                                                       break;
+                                                                       case 1:
+                                                                                       #${$var}[$array[0]] = $val;
+                                                                                       $cmdlinevars[${$var}][$array[0]] = $val;
+                                                                                       break;
+                                                       }
+                                               }
+                                               elseif (is_array($confvars[$var])) {  // old-style array settings
+                                                       $cmdlinevars["$var"][] = $val;
+                                               }
+                                               else {
+                                                       $cmdlinevars["$var"] = $val;
+                                               }
+                                       }
+                                       else {
+                                               $confvars['error'][] = "Unknown command-line option --$var";
+                                               $cmdline = FALSE;
+                                       }
+                               }
+                               elseif ((substr($arg, 0, 1) == '-') && (strlen($arg) == 2) && (! $optsdone)) {
+                                       // Try to parse single letter, single dash options
+                                       if (in_array(substr($arg, 1), $confvars)) { // Does not accept value
+                                               $cmdlinevars["$var"] = TRUE;
+                                       }
+                                       elseif (array_key_exists(substr($arg, 1).':', $confvars)) { // Required value
+                                               if (($argnum + 1 >= $argc) || (substr($argv[$argnum + 1], 0, 1) == '-')) {
+                                                       $confvars['error'][] = "Required value for $arg not given.";
+                                                       $cmdline = FALSE;
+                                               }
+                                               else {
+                                                       $var = $confvars[substr($arg, 1).':'];
+                                                       $val = trim($argv[$argnum + 1]);
+                                                       $skip = TRUE;
+                                                       $cmdlinevars["$var"] = $val;
+                                               }
+                                       }
+                                       elseif (array_key_exists(substr($arg, 1).'::', $confvars)) { // Optional value
+                                               if (($argnum + 1 < $argc) && (substr($argv[$argnum + 1], 0, 1) != '-')) {
+                                                       $var = $confvars[substr($arg, 1).'::'];
+                                                       $val = trim($argv[$argnum + 1]);
+                                                       $skip = TRUE;
+                                                       $cmdlinevars["$var"] = $val;
+                                               }
+                                               else {
+                                                       $var = $confvars[substr($arg, 1).'::'];
+                                                       $cmdlinevars["$var"] = TRUE;
+                                               }
+                                       }
+                                       else {
+                                               $confvars['error'][] = "Unknown command-line option -$var";
+                                               $cmdline = FALSE;
+                                       }
+                               }
+                               else { // else it's a command argument
+                                       $cmdlineargs[]=$arg;
+                               }
+                       }
+               }
+               $confvars['arguments'] = $cmdlineargs;
+       }
+
+       // Set configuration settings with settings from config file.
+       // If config not set by command-line, look for it in the file system.
+       if (isset($cmdlinevars) && array_key_exists('config', $cmdlinevars)) {
+               if (file_exists($cmdlinevars('config'))) $config = $cmdlinevars('config');
+               else $config = FALSE;
+       }
+       else $config = config_location($confvars); // Returns config file pathname or FALSE
+
+       if ($config['path']) {
+               $confvars['config'] = $config['path'];
+               $confvars['embedded'] = $config['embedded'];
+               $lines = file($config['path']);
+               foreach ($lines as $key => $line) {
+                       $line=trim($line);
+                       // Ignore blank lines and lines beginning with #, //, or ;
+                       if (($line != '') && (substr($line, 0, 1) != '#') && (substr($line, 0, 2) != '//') && (substr($line, 0, 1) != ';')) {
+                               $sep = strpos($line, '=');
+                               if ($sep === FALSE) { // This is an implicit setting of TRUE
+                                       $var = $line;
+                                       $val = TRUE;
+                               }
+                               else { // This is set to a value
+                                       $val = trim(substr($line, $sep + 1));
+                                       $var = trim(substr($line, 0, $sep));
+                                       if (substr($var, -1, 1) == ']') {
+                                                       $array=array();
+                                                       $indexes = substr($var, strpos($var, '['));
+                                                       $var = substr($var, 0, strpos($var, '['));
+                                                       while (strpos($indexes, '[') !== FALSE) {
+                                                                       $b = strpos($indexes, ']');
+                                                                       $array[] = substr($indexes, 1, $b -1);
+                                                                       $indexes = substr($indexes, $b + 1);
+                                                       }
+                                       }
+                               }
+                               if (array_key_exists($var, $confvars)) {
+                                       if ($val == '') $val = NULL;
+                                       if (strtoupper($val) == 'NULL') $val = NULL;
+                                       if (strtoupper($val) == 'TRUE') $val = TRUE;
+                                       if (strtoupper($val) == 'FALSE') $val = FALSE;
+                                       if (isset($array)) {
+                                               switch (count($array)) {
+                                                               case 5:
+                                                                               $confvars[$var][$array[0]][$array[1]][$array[2]][$array[3]][$array[4]] = $val;
+                                                                               break;
+                                                               case 4:
+                                                                               $confvars[$var][$array[0]][$array[1]][$array[2]][$array[3]] = $val;
+                                                                               break;
+                                                               case 3:
+                                                                               $confvars[$var][$array[0]][$array[1]][$array[2]] = $val;
+                                                                               break;
+                                                               case 2:
+                                                                               $confvars[$var][$array[0]][$array[1]] = $val;
+                                                                               break;
+                                                               case 1:
+                                                                               #${$var}[$array[0]] = $val;
+                                                                               $confvars[$var][$array[0]] = $val;
+                                                                               break;
+                                               }
+                                       }
+                                       elseif (is_array($confvars[$var])) { // old-style array settings
+                                               $confvars["$var"][] = $val;
+                                       }
+                                       else {
+                                               $confvars["$var"] = $val;
+                                       }
+                               }
+                               elseif (! in_array($var, $confvars['unused'])) {
+                                       $confvars['error'][] = "Unknown configuration file setting: $var";
+                                       $config = FALSE;
+                               }
+                       }
+               }
+       }
+       elseif ($confvars['mustconf']) { // If must have config file, return FALSE now
+               $confvars['error'] = array(CANT_OPEN_CONF);
+               return FALSE;
+       }
+
+       if ($argv) {
+               // Override all previous settings via any command-line args
+               $confvars = array_merge($confvars, $cmdlinevars);
+       }
+
+       // Do stuff local to this script
+       $defaults = init_settings_local($confvars);
+
+       // Resolve variables, ie: variable=$someothervariable
+       do {
+               $unresolved = FALSE;
+               foreach ($confvars as $key => $value) {
+                       if ((! is_array($value)) && (! is_resource($value))) {
+                               if (substr($value, 0, 1) == '$') {
+                                       $var = substr($value, 1);
+                                       if (array_key_exists($var, $confvars)) {
+                                               $confvars[$key] = $confvars[$var];
+                                               if (substr($confvars[$var], 0, 1) == '$') $unresolved = TRUE;
+                                       }
+                               }
+                       }
+               }
+       } while ($unresolved);
+       return (($defaults && $config && $cmdline) ? TRUE : FALSE);
+}
+
+/**
+ * Localized extensions called by init_settings
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @return bool TRUE on success or FALSE
+ */
+function init_settings_local(&$confvars) {
+       // Application-specific code called by init_settings()
+       // returns FALSE on error and message in $confvars['error']
+       $return = TRUE;
+
+       if ((($confvars['debug'] == NULL) ||
+               (trim($confvars['debug']) == '')) && ($confvars['debug'] !== FALSE)) $confvars['debug'] = FALSE; // Don't print debug messages
+       if (is_resource(STDIN)) { // STDIN will not be a resource if we've already closed it
+               // These can't be changed after daemonization.  Restart to change standard file handles.
+               $confvars['stdin'] = '/dev/null'; // Don't allow stdin to be pointed away from /dev/null
+               if (($confvars['stdout'] == NULL) || (trim($confvars['stdout']) == '')) $confvars['stdout'] = '/dev/null';
+               if (($confvars['stderr'] == NULL) || (trim($confvars['stderr']) == '')) $confvars['stderr'] = '/dev/null';
+       }
+       if ($confvars['checkdelay'] == NULL) $confvars['checkdelay'] = 2;
+       if ($confvars['silentmax'] == NULL) $confvars['silentmax'] = 60;
+       if ($confvars['pregex'] == NULL)  $confvars['pregex'] =
+               '([A-Z][a-z][a-z]) ([ 0-9][0-9]) ([0-9][0-9]:[0-9][0-9]:[0-9][0-9]) ([A-Za-z0-9]+) .* ';
+       if ($confvars['firewall'] == NULL) $confvars['firewall'] =
+               array('/usr/sbin/iptables -N phailomatic-$1', '/usr/sbin/iptables -A phailomatic-$1 -j RETURN',
+               '/usr/sbin/iptables -I INPUT -p tcp --dport $1 -j phailomatic-$1');
+       if ($confvars['ban'] == NULL) $confvars['ban'] = '/usr/sbin/iptables -I phailomatic-$1 -s $2 -j DROP';
+       if ($confvars['unban'] == NULL) $confvars['unban'] = '/usr/sbin/iptables -D phailomatic-$1 -s $2 -j DROP';
+       if ($confvars['teardown'] == NULL) $confvars['teardown'] =
+               array('/usr/sbin/iptables -D INPUT -p tcp --dport $1 -j phailomatic-$1',
+               '/usr/sbin/iptables -F phailomatic-$1', '/usr/sbin/iptables -X phailomatic-$1');
+       for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+               if ($confvars['file'.$i] == NULL) continue;
+               if (! isset($confvars['file'.$i]['syslogformat'])) $confvars['file'.$i]['syslogformat'] = TRUE;
+               foreach ($confvars['file'.$i]['regex'] as $key => $rule) {
+                       if (! isset($rule['ports'])) {
+                               $confvars['error'][] = 'Ports to firewall missing for '.$confvars['file'.$i].' regex['.$key.']';
+                               $return = FALSE;
+                       }
+                       elseif (! is_array($rule['ports'])) {
+                               $rule['ports'] = str_replace(' ', '', $rule['ports']);
+                               $confvars['file'.$i]['regex'][$key]['ports'] = explode(',', $rule['ports']);
+                       }
+
+                       if (! isset($rule['timeout'])) $confvars['file'.$i]['regex'][$key]['timeout'] = 600;
+                       if (! isset($rule['bantrigger'])) $confvars['file'.$i]['regex'][$key]['bantrigger'] = 5;
+                       if (! isset($rule['hitperiod'])) $confvars['file'.$i]['regex'][$key]['hitperiod'] = 90;
+               }
+       }
+       return $return;
+}
+
+/**
+ * Daemonize
+ *
+ * @package phailomatic
+ * @param string $myname name of the running script
+ * @param array $error error messages passed back
+ * @param string $pidpath path to PIDfile
+ * @param array $ignoresigs array of signals to ignore
+ * @param int $stdin STDIN
+ * @param int $stdout STDOUT
+ * @param int $stderr STDERR
+ * @return bool TRUE on success or FALSE
+ */
+function proc_daemonize($myname, &$error, $pidpath, $ignoresigs, &$stdin, &$stdout, &$stderr) {
+       // Returns file descriptors in place of file paths for $stdin, $stdout, $stderr
+
+       // Fork, become session leader, fork again, and close open handles so there's no zombie.
+       // Open lock file
+       $lock = fopen($pidpath.'/'.$myname.'.pid', 'c+');
+       if (! flock($lock, LOCK_EX | LOCK_NB)) {
+               $error = ALREADY_RUNNING;
+               return FALSE;
+       }
+
+       // Fork. If we get a PID, exit.  If we get 0, we're the child, continue
+       if (pcntl_fork()) exit();
+
+       // Dissociate from controlling terminal, become session leader
+       if (posix_setsid() === -1) {
+               $error = CANT_SETSID;
+               return FALSE;
+       }
+       usleep(100000); // sleep 1/10th of a second
+
+       // Fork again as session leader to be free of other processes. If pcntl_fork
+       // returns 0 we're the child, else we're the parent getting the PID of the child
+       $childpid = pcntl_fork();
+
+       if ($childpid) {
+               // If we get here, we are the parent, write PID to pidfile
+               fseek($lock, 0);
+               ftruncate($lock, 0);
+               fwrite($lock, $childpid);
+               fflush($lock);
+               closelog();
+               exit();
+       }
+       // If we get here we're the child finally independent, Grab lockfile.
+       else flock($lock, LOCK_EX|LOCK_NB); // Grab lockfile.
+
+       // As we are a daemon, close and re-open standard file descriptors.
+       proc_init_std_file_descriptors($stdin, $stdout, $stderr);
+
+       // Ignore some signals we don't care about
+       foreach ($ignoresigs as $ignoresig) pcntl_signal($ignoresig, SIG_IGN);
+       return $lock;
+}
+
+/**
+ * Initialize standard file descriptors
+ *
+ * @package phailomatic
+ * @param int $stdin STDIN
+ * @param int $stdout STDOUT
+ * @param int $stderr STDERR
+ */
+function proc_init_std_file_descriptors(&$stdin, &$stdout, &$stderr) {
+
+       if (is_resource(STDIN)) fclose(STDIN);
+       if (is_resource(STDOUT)) fclose(STDOUT);
+       if (is_resource(STDERR)) fclose(STDERR);
+       if (is_resource($stdin)) fclose($stdin);
+       if (is_resource($stdout)) fclose($stdout);
+       if (is_resource($stderr)) fclose($stderr);
+
+       // http://andytson.com/blog/2010/05/daemonising-a-php-cli-script-on-a-posix-system/
+       // When a standard file descriptor is closed, it can be replaced.
+       // Create new standard file descriptors in case anything tries to use them.
+       // Variable names are not important, but do not re-order the fopens
+       if ($stdout == $stderr) $stderr = 'php://stdout'; // hack to duplicate fd1 to fd2
+       $stdin = fopen('/dev/null', 'r'); // set fd0
+       $stdout = fopen($stdout, 'w'); // set fd1
+       $stderr = fopen($stderr, 'w'); // set fd2 or set fd2 to fd1
+}
+
+/**
+ * Log message to syslog
+ *
+ * @package phailomatic
+ * @param int $prio syslog message priority
+ * @param array $vars configuration variables
+ * @param array $msgs messages to send to syslog
+ * @param int $stderr STDERR
+ */
+function log_msgs($prio, $vars, $msgs, $stderr=FALSE) {
+       foreach ($msgs as $msg) log_msg($prio, $vars, $msg, $stderr);
+}
+
+/**
+ * Log message to syslog
+ *
+ * @package phailomatic
+ * @param int $prio syslog message priority
+ * @param array $vars configuration variables
+ * @param string $msg message to send to syslog
+ * @param int $stderr STDERR
+ */
+function log_msg($prio, $vars, $msg, $stderr=FALSE) {
+       if ($vars['debug']) $msg .= ' mem: '.memory_get_usage().' max: '.memory_get_peak_usage();
+       syslog($prio, $msg);
+       if ($stderr === TRUE) @fwrite($vars['stderr'], date('M j H:i:s ').$msg."\n");
+}
+?>
diff --git a/phailomatic.1.gz b/phailomatic.1.gz
new file mode 100644 (file)
index 0000000..71011a4
Binary files /dev/null and b/phailomatic.1.gz differ
diff --git a/phailomatic.conf.sample b/phailomatic.conf.sample
new file mode 100644 (file)
index 0000000..41c0eb2
--- /dev/null
@@ -0,0 +1,48 @@
+# Phailomatic configuration file
+#
+# Regexes must match as a named capture the IP address of the connecting client,
+# and begin following the syslog date and severity.  For example in the line:
+# Feb  6 00:30:25 localhost mail.info postfix/smtpd[23527]: ...
+# regexes should start matching with "postfix" and continue to the end of the line.
+# Please note that Phailomatic *requires* you to match to the end of the line,
+# but DO NOT include the end of line marker ($) in your regex.
+#
+# Each firewall/teardown command will be executed in order
+# Firewall command substitutions: $1 = port number, $2 = IP address
+# firewall[1]=iptables -N phailomatic-$1
+# firewall[2]=iptables -A phailomatic-$1 -j RETURN
+# firewall[3]=iptables -I INPUT -p tcp --dport $1 -j phailomatic-$1
+# 
+# Triggers: Phailomatic can either call a PHP function or execute a commandline or both.
+# trigger[include]=/path/to/yourcode.php
+# This file will be included into Phailomatic and must contain:
+# function phailtrigger($ip, $ports) // IP and array of port numbers will be passed to this function when something is blocked
+#
+# trigger[exec]=/path/to/trigger $2 $1
+# when we execute this commandline, $2 will be replaced by IP, $1 by string of ports separated by commas
+#
+# Each file can contain multiple regex definitions.
+#  
+# file1[path]=/var/log/messages
+# file1[syslogformat]=TRUE
+# file1[regex][1][pattern]=dropbear\[[0-9]+\]: (?:bad password|login) attempt for .* from ::ffff:(?P<ip>[0-9.]+):[0-9]+
+# file1[regex][1][ports]=22
+# file1[regex][1][hitperiod]=90
+# file1[regex][1][timeout]=600
+# file1[regex][1][bantrigger]=5
+# file1[regex][2][pattern]=postfix/smtpd\[[0-9]+\]: .*\[(?P<ip>[0-9.]+)\]: SASL (?:[A-Z]+) authentication failed: authentication failure
+# file1[regex][2][ports]=25,587,2525
+
+debug=TRUE
+
+file1[path]=/var/log/messages
+file1[regex][1][pattern]=dropbear\[[0-9]+\]: (?:bad password|login) attempt for .* from ::ffff:(?P<ip>[0-9.]+):[0-9]+
+file1[regex][1][ports]=22
+file1[regex][1][hitperiod]=90
+file1[regex][1][bantrigger]=3
+file1[regex][1][timeout]=600
+file1[regex][2][pattern]=postfix/smtpd\[[0-9]+\]: .*\[(?P<ip>[0-9.]+)\]: SASL (?:[A-Z]+) authentication failed: authentication failure
+file1[regex][2][ports]=25,587,2525
+file1[regex][2][hitperiod]=90
+file1[regex][2][bantrigger]=3
+file1[regex][2][timeout]=600