From 86b26b9cb4b636f0411cf37fae29b1faec6ff838 Mon Sep 17 00:00:00 2001 From: Gitolite Admin Date: Sun, 9 Feb 2014 01:41:55 -0500 Subject: [PATCH 1/1] Initial check-in --- CHANGELOG | 0 README | 37 ++ phailomatic | 913 ++++++++++++++++++++++++++++++++++++++++ phailomatic.1.gz | Bin 0 -> 2657 bytes phailomatic.conf.sample | 48 +++ 5 files changed, 998 insertions(+) create mode 100644 CHANGELOG create mode 100644 README create mode 100755 phailomatic create mode 100644 phailomatic.1.gz create mode 100644 phailomatic.conf.sample diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..e69de29 diff --git a/README b/README new file mode 100644 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 index 0000000..6481bdf --- /dev/null +++ b/phailomatic @@ -0,0 +1,913 @@ +#!/usr/bin/env php + + * + * 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 + * @license http://www.fsf.org/licenses/gpl.html GNU Public License + * @copyright Copyright © 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[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[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 index 0000000000000000000000000000000000000000..71011a47c7f21cfdf2b81ec6ff79c9a568a37de6 GIT binary patch literal 2657 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[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[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[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[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 -- 2.39.5