#!/usr/bin/perl -w
#######################################################
# monitorNetStats.pl
# Written by: Brandon Zehm <caspian@dotconf.net>
# 
# Purpose:
#   To monitor various network related things, and 
#   syslog that information.
#
# Technical Description:
#   Reads data out of /proc and generates output.
# 
# Changelog
#   01/20/2004 - v1.20
#         - Require a -r option to run.
#         - Added a -l option for logging to a file.
#         - Textual changes to most error messages.
#         - Replaced some system calls with perl.
# 
#######################################################
use strict;

## Global Variable(s)
my %conf = (
    "programName"          => 'monitorNetStats.pl',
    "version"              => '1.20',
    "authorName"           => 'Brandon Zehm',                    ## Information about the author or owner of this script.
    "authorEmail"          => 'caspian@dotconf.net',
    "hostname"             => $ENV{'HOSTNAME'},                  ## Used in printmsg() for all output.
    
    "syslogProgram"        => "/usr/local/bin/syslog.pl",
    
    "tempFileThroughput"   => "/tmp/monitorNetStats",
    
    "run"                  => 0,                                 ## Whether we should run script or not
    "debug"                => 0,
    "syslog"               => 1,                                 ## Syslog messages by default (--stdout disables this)
    "stdout"               => 0,
    "facility"             => "USER",
    "priority"             => "INFO",
    "logging"              => '',                                ## If this is true the printmsg function prints to the log file
    "logFile"              => '',                                ## If this is specified (form the command line via -l) this file will be used for logging.
    
);







#############################
##
##      MAIN PROGRAM
##
#############################

## Initialize
initialize();

## Process Command Line
processCommandLine();

## Open the log file if we need to
if ($conf{'logFile'}) {
    if ($conf{'logging'}) {
        $conf{'logging'} = 0;
        close LOGFILE;
    }
    if (openLogFile($conf{'logFile'})) { quit("OS-ERROR => Opening the log file [$conf{'logFile'}] returned the error: $!", 1); }
}


## Get network devices from /proc/net/dev
my @lines = ();
for (my $counter = 0; $counter <= 10; $counter++, sleep(10)) {
    quit("OS-ERROR => 10 consecutive errors while trying to read /proc/net/dev.  Last error: $!", 1) if ($counter >= 10);
    open(DEV, "< /proc/net/dev") or next;
    @lines = <DEV>;
    close DEV;
    chomp @lines;
    last if ($lines[0]);
}

## Monitor network throughput for all network devices
@lines = grep(/^\s*(eth|bond)/,@lines);
foreach my $line (@lines) {
    if ($line !~ /^(\s*)(.*)(:.*)$/) {
        printmsg("OS-DEBUG => The line [$line] didn't appear to have valid info.", 1);
    }
    else {
        ## Get info for this network device
        $conf{'message'} = ("OS-$conf{'priority'} => " . monitorThroughput($2, $conf{'tempFileThroughput'}) );
        
        ## Syslog the message (or print it to stdout)
        printmsg($conf{'message'}, 0);
    }
}




## Monitor ip_conntrack stuff
if ( -e "/proc/net/ip_conntrack") {
    ## Get values from proc
    my $currentConnections = `cat /proc/net/ip_conntrack | wc -l`; chomp $currentConnections; $currentConnections =~ s/\s+//g;
    my $maxMsg = "";
    my $maxFile = "";
    if ( -e "/proc/sys/net/ipv4/ip_conntrack_max") { $maxFile = "/proc/sys/net/ipv4/ip_conntrack_max"; }
    if ( -e "/proc/sys/net/ipv4/netfilter/ip_conntrack_max") { $maxFile = "/proc/sys/net/ipv4/netfilter/ip_conntrack_max"; }
    if ( -e "/proc/sys/net/nf_conntrack_max") { $maxFile = "/proc/sys/net/nf_conntrack_max"; }
    if ($maxFile ne "") {
        my $maxConnections = `cat '$maxFile'`; chomp $maxConnections;
        $maxMsg = sprintf(", %.02f percent of it's capacity.", (($currentConnections / $maxConnections) * 100) );
    }
    
    ## Generate a message
    $conf{'message'} = "OS-$conf{'priority'} => iptables is currently tracking $currentConnections connections".$maxMsg;
    
    ## Syslog the message (or print it to stdout)
    printmsg($conf{'message'}, 0);
}



## Quit
quit("",0);
































######################################################################
## Function:    help ()
##
## Description: For all those newbies ;) 
##              Prints a help message and exits the program.
## 
######################################################################
sub help {
print <<EOM;

$conf{'programName'}-$conf{'version'} by $conf{'authorName'} <$conf{'authorEmail'}>

Monitors network throughput and iptables connection tracking usage. 
The system syslog is used for any generated messages unless the --stdout 
option is used.

Usage:  $conf{'programName'} [options]

  Required:
    -r                        run script
  
  Optional:
    --stdout                  print messages to STDOUT rather than the syslog.
    --facility=[0-11]         syslog facility (1/USER is used by default)
    -l <logfile>              enable logging to the specified file
    -v                        verbosity - use multiple times for greater effect

EOM
quit("", 1);
}










######################################################################
##  Function: initialize ()
##  
##  Does all the script startup jibberish.
##  
######################################################################
sub initialize {

    ## Set STDOUT to flush immediatly after each print  
    $| = 1;
 
    ## Intercept signals
    $SIG{'QUIT'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'INT'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'KILL'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    $SIG{'TERM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
  
    ## ALARM and HUP signals are not supported in Win32
    unless ($^O =~ /win/i) {
        $SIG{'HUP'}   = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
        $SIG{'ALRM'}  = sub { quit("$$ - EXITING: Received SIG$_[0]", 1); };
    }
  
    ## Make sure the path is sane
    $ENV{'PATH'} .= ":/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin";
  
    ## Fixup $conf{'hostname'}
    if ($conf{'hostname'}) {
        $conf{'hostname'} = $conf{'hostname'};
        $conf{'hostname'} =~ s/\..*$//;
    }
    else {
        $conf{'hostname'} = "unknown";
    }
  
    return(1);
}









######################################################################
##  Function: processCommandLine ()
##  
##  Processes command line storing important data in global var %conf
##  
######################################################################
sub processCommandLine {
  
  
    ############################
    ##  Process command line  ##
    ############################
   
    my @ARGS = @ARGV;
    my $numargv = @ARGS;
    my $counter = 0;
    for ($counter = 0; $counter < $numargv; $counter++) {
  
        if ($ARGS[$counter] =~ s/^--stdout//i) {             ## stdout ##
            $conf{'stdout'} = 1;
            $conf{'syslog'} = 0;
        }
        
        elsif ($ARGS[$counter] =~ /^-r$/) {                  ## Run
            $conf{'run'} = 1;
        }
        
        elsif ($ARGS[$counter] =~ /^-l$/) {                  ## Log File ##
            $counter++;
            $conf{'logFile'} = $ARGS[$counter];
        }
        
        elsif ($ARGS[$counter] =~ s/^--facility=//i) {       ## Facility ##
            $conf{'facility'} = $';
        }

        elsif ($ARGS[$counter] =~ s/^-v+//i) {               ## Verbosity ##
            $conf{'debug'} += (length($&) - 1);
        }
        
        else {                                               ## Unknown Option ##
            quit("OS-ERROR => The option [$ARGS[$counter]] is unknown. Try --help", 1);
        }

    }
    
    ## If the user didn't use a -r print the help
    if ($conf{'run'} == 0) {
        help();
    }
  
    return(0);
}

















######################################################################
##  Function:    monitorThroughput (string $device, string $tempFile)
##  
##  Description: 
##               
##               
##
##  Example:     
######################################################################
sub monitorThroughput {
    my %incoming = ();
    (
      $incoming{'device'},
      $incoming{'tempFile'}
    ) = @_;
    
    $incoming{'tempFile'} .= ".$incoming{'device'}";
    my $inputBytes = " ";
    my $outputBytes = " ";
    my $currentTime = time();
    my $transferRate;
    my $lastInputBytes;
    my $lastOutputBytes;
    my $lastTime;
    my $null;
    my $stats;
    
    ## Check to see that /proc/net/dev exists
    if (! -f "/proc/net/dev") {
        return("OS-WARNING => /proc/net/dev does not exist");
    }
    
    ## Get raw data from /proc
    until (($inputBytes =~ /\d/) and ($outputBytes =~ /\d/)) {
        open(FILE, "/proc/net/dev") or return("OS-WARNING => Error while opening /proc/net/dev: $!");
        my @array = <FILE>;
        ($stats) = grep(/^\s*$incoming{'device'}:/, @array);
        chomp $stats;
        return("No statistics available for device: $incoming{'device'}") if ($stats =~ /No statistics available/i);
        
        $stats =~ s/^\s*$incoming{'device'}:\s*//;
        
        ## Get current IN and OUT bytes
        ($inputBytes,$null,$null,$null,$null,$null,$null,$null,$outputBytes) = split(/\s+/, $stats);
    }
    
    
    ## Get data from the temp file, and write the new values
    if ( -f $incoming{'tempFile'} ) {
        
        open(FILE, $incoming{'tempFile'}) or quit("OS-ERROR => Error while opening the file [$incoming{'tempFile'}].  The error was: $!", 1);
        
        ($lastTime, $lastInputBytes, $lastOutputBytes) = split(/:/, <FILE>);
        
        ## Write new values to tempFile
        open (FILE, ">$incoming{'tempFile'}") or quit("OS-ERROR => Error while opening the file [$incoming{'tempFile'}].  The error was: $!", 1);
        print FILE time() . ":$inputBytes:$outputBytes\n";
        close FILE;
        
        ## Deal with the 4GB counter limitation of the Linux kernel.
        if ($lastInputBytes > $inputBytes) { $inputBytes += (4294967296 - $lastInputBytes); $lastInputBytes = 0; };
        if ($lastOutputBytes > $outputBytes) { $outputBytes += (4294967296 - $lastOutputBytes); $lastOutputBytes = 0; };
        
        ## Here we re-assign these three variables to be the difference from last time... I know it's evil.
        $currentTime = ($currentTime - $lastTime);
        $inputBytes = ($inputBytes - $lastInputBytes);
        $outputBytes = ($outputBytes - $lastOutputBytes);
        
        ## Calculate the difference between the last read and now, and calculate that into KBytes/second
        $transferRate = ((($inputBytes + $outputBytes) / $currentTime) / 1024);
    }
    
    ## Write the current data to the temp file if its a new file
    else {
        printmsg("NOTICE => The file [$incoming{'tempFile'}] does not exist.  Creating it now with mode [0600].", 0); 
        open (FILE, ">$incoming{'tempFile'}") or quit("OS-ERROR => Error while creating the file [$incoming{'tempFile'}].  The error was: $!");
        
        print FILE time() . ":$inputBytes:$outputBytes\n";
        close FILE;
        chmod (0600, $incoming{'tempFile'});
        
        $currentTime = 0;
        $inputBytes = 0;
        $outputBytes = 0;
        $transferRate = 0;
    }
    
    return(sprintf("Period: $currentTime seconds  Interface: $incoming{'device'}  Input Bytes: $inputBytes  Output Bytes: $outputBytes  Transfer Rate: %.02f KB/s", $transferRate));
}
















###############################################################################################
##  Function:    printmsg (string $message, int $level)
##
##  Description: Handles all messages - 
##               Depending on the state of the program it will log
##               messages to a log file, print them to STDOUT or both.
##               
##
##  Input:       $message          A message to be printed, logged, etc.
##               $level            The debug level of the message. If not defined 0
##                                 will be assumed.  0 is considered a normal message, 
##                                 1 and higher is considered a debug message.
##  
##  Output:      Prints to STDOUT, to LOGFILE, both, or none depending 
##               on the state of the program and the debug level specified.
##  
##  Example:     printmsg("ERROR => The file could not be opened!", 0);
###############################################################################################
sub printmsg {
    ## Assign incoming parameters to variables
    my ( $message, $level ) = @_;
    
    ## Make sure input is sane
    $level = 0 if (!defined($level));
    
    ## Continue only if the debug level of the program is >= message debug level.
    if ($conf{'debug'} >= $level) {
        
        ## Get the date in the format: Dec  3 11:14:04
        my ($sec, $min, $hour, $mday, $mon) = localtime();
        $mon = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[$mon];
        my $date = sprintf("%s %02d %02d:%02d:%02d", $mon, $mday, $hour, $min, $sec);
    
        ## Syslog the message is needed
        if ($conf{'syslog'}) {
            system( qq{$conf{'syslogProgram'} --facility=$conf{'facility'} --priority=$conf{'priority'} "$conf{'programName'}: $message" });
        }
        
        ## Print to STDOUT always if debugging is enabled, or if conf{stdout} is true.
        if ( ($conf{'debug'} >= 1) or ($conf{'stdout'} == 1) ) {
            print "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
        ## Print to the log file if $conf{'logging'} is true
        if ($conf{'logging'}) {
            print LOGFILE "$date $conf{'hostname'} $conf{'programName'}: $message\n";
        }
        
    }
    
    ## Return 0 errors
    return(0);
}














###############################################################################################
## FUNCTION:    
##   openLogFile ( $filename )
## 
## 
## DESCRIPTION: 
##   Opens the file $filename and attaches it to the filehandle "LOGFILE".  Returns 0 on success
##   and non-zero on failure.  Error codes are listed below, and the error message gets set in
##   global variable $!.
##   
##   
## Example: 
##   openFile ("/var/log/scanAlert.log");
##
###############################################################################################
sub openLogFile {
    ## Get the incoming filename
    my $filename = $_[0];
    
    ## Make sure our file exists, and if the file doesn't exist then create it
    if ( ! -f $filename ) {
        printmsg("NOTICE => The file [$filename] does not exist.  Creating it now with mode [0600].", 0);
        open (LOGFILE, ">>$filename");
        close LOGFILE;
        chmod (0600, $filename);
    }
    
    ## Now open the file and attach it to a filehandle
    open (LOGFILE,">>$filename") or return (1);
    
    ## Put the file into non-buffering mode
    select LOGFILE;
    $| = 1;
    select STDOUT;
    
    ## Tell the rest of the program that we can log now
    $conf{'logging'} = "yes";
    
    ## Return success
    return(0);
}











######################################################################
##  Function:    quit (string $message, int $errorLevel)
##  
##  Description: Exits the program, optionally printing $message.  It 
##               returns an exit error level of $errorLevel to the 
##               system  (0 means no errors, and is assumed if empty.)
##
##  Example:     quit("Exiting program normally", 0);
######################################################################
sub quit {
  my %incoming = ();
  (
    $incoming{'message'},
    $incoming{'errorLevel'}
  ) = @_;
  $incoming{'errorLevel'} = 0 if (!defined($incoming{'errorLevel'}));
  
  
  ## Print exit message
  if ($incoming{'message'}) { 
    printmsg($incoming{'message'}, 0);
  }
  
  ## Exit
  exit($incoming{'errorLevel'});
}






