#!perl

# check_updates is a Nagios plugin to check if RedHat or Fedora system
# is up-to-date
#
# See  the INSTALL file for installation instructions
#
# Copyright (c) 2007, ETH Zurich.
#
# This module is free software; you can redistribute it and/or modify it
# under the terms of GNU general public license (gpl) version 3.
# See the LICENSE file for details.
#
# RCS information
# enable substitution with:
#   $ svn propset svn:keywords "Id Revision HeadURL Source Date"
#
#   $Id: check_connections 1121 2010-02-01 07:20:36Z corti $
#   $Revision: 1121 $
#   $HeadURL: https://svn.id.ethz.ch/nagios_plugins/check_connections/check_connections $
#   $Date: 2010-02-01 08:20:36 +0100 (Mon, 01 Feb 2010) $

use strict;
use warnings;

use Carp;
use English '-no_match_vars';
use File::Slurp;
use Nagios::Plugin::Threshold;
use Nagios::Plugin;
use Nagios::Plugin::Getopt;
use version;

our $VERSION = '2.1.1';

# IMPORTANT: Nagios plugins could be executed using embedded perl in this case
#            the main routine would be executed as a subroutine and all the
#            declared subroutines would therefore be inner subroutines
#            This will cause all the global lexical variables not to stay shared
#            in the subroutines!
#
# All variables are therefore declared as package variables...
#
use vars qw(
               $command
               $help
               $icmp
               $icmp_in
               @lines
               $local
               $netstat
               $options
               $output
               $pid
               $plugin
               $protocol
               $recv
               $remote
               $send
               $state
               %states
               $status
               $status_msg
               $tcp
               $threshold
               $total
               $udp
               $udp_in
               $verbosity
);

##############################################################################
# subroutines

##############################################################################
# Usage     : verbose("some message string", $optional_verbosity_level);
# Purpose   : write a message if the verbosity level is high enough
# Returns   : n/a
# Arguments : message : message string
#             level   : options verbosity level
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub verbose {

    # arguments
    my $message = shift;
    my $level   = shift;

    if ( !defined $message ) {
        $plugin->nagios_exit( UNKNOWN,
            q{Internal error: not enough parameters for 'verbose'} );
    }

    if ( !defined $level ) {
        $level = 0;
    }

    if ( $level < $verbosity ) {
        print $message;
    }

    return;

}

##############################################################################
# Usage     : $state = standardize_state( $state );
# Purpose   : convert connection states to a standard set (not all the OSes
#             have the same set of states)
# Returns   : a standard state
# Arguments : $state : state string to standardize
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub standardize_state {

    my $state = shift;

# Linux        Mac OS X     SunOS
#
#                           BOUND        ready to connect or listen
# CLOSED       CLOSED       CLOSED       socket not being used
# CLOSE_WAIT   CLOSE_WAIT   CLOSE_WAIT   remote end has shut down, waiting for socket to close
# CLOSING      CLOSING      CLOSING      both sockets closed but not all data sent
# ESTABLISHED  ESTABLISHED  ESTABLISHED  connection established
# FIN_WAIT1    FIN_WAIT1    FIN_WAIT1    socket closed connection shutting down
# FIN_WAIT2    FIN_WAIT2    FIN_WAIT2    connection closed waiting from the other end
#                           IDLE         opened but not bound
# LAST_ACK     LAST_ACK     LAST_ACK     remote end has shut down, socket closed, wait for ack
# LISTEN       LISTEN       LISTEN       listening for incoming connections
# SYN_RECV     SYN_RCVD     SYN_RECEIVED connection request received
# SYN_SENT     SYN_SENT     SYN_SENT     attempting to establish a connection
# TIME_WAIT    TIME_WAIT    TIME_WAIT    wait after close for packets still in the network
# UNKNOWN                                state unknown

    # Remapping

    # SYN_RECV, SYN_RCVD, SYN_RECEIVED -> SYN_RECEIVED
    if ( $state =~ /^(SYN_RECV|SYN_RCVD)$/mxs ) {
        return 'SYN_RECEIVED';
    }

    # FIN_WAIT_[12] -> FIN_WAIT\1
    if ( $state =~ /^FIN_WAIT_([1-2])$/mxs ) {
        return "FIN_WAIT$1";
    }

    return $state;

}

##############################################################################
# Usage     : initialize_state_table
# Purpose   : resets the counters for each known state
# Returns   : n/a
# Arguments : n/a
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub initialize_state_table {

    my @known_states = qw(
      BOUND
      CLOSED
      CLOSE_WAIT
      CLOSING
      ESTABLISHED
      FIN_WAIT1
      FIN_WAIT2
      IDLE
      LAST_ACK
      LISTEN
      SYN_RECEIVED
      SYN_SENT
      TIME_WAIT
      UNKNOWN
    );

    for my $state (@known_states) {
        $states{$state} = 0;
    }

    return;

}

##############################################################################
# Usage     : check_positive_integer($number)
# Purpose   : checks if the argument is a valid positive integer
# Returns   : true if the number is valid
# Arguments : number to test
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub check_positive_integer {
    my $number = shift;
    return $number =~ /^[0-9]+$/mxs;
}

##############################################################################
# Usage     : get_path('program_name');
# Purpose   : retrieves the path of an executable file using the
#             'which' utility
# Returns   : the path of the program (if found)
# Arguments : the program name
# Throws    : n/a
# Comments  : n/a
# See also  : n/a
sub get_path {

    my $prog = shift;
    my $path;

    my $which_command = "which $prog";
    my $which_output;

    open $which_output, q{-|}, "$which_command 2>&1"
      or $plugin->nagios_exit( UNKNOWN,
        "Cannot execute $which_command: $OS_ERROR" );

    while (<$which_output>) {
        chomp;
        if ( !/^which:/mxs ) {
            $path = $_;
        }
    }

    if (  !( close $which_output )
        && ( $OS_ERROR != 0 ) )
    {

        # close to a piped open return false if the command with non-zero
        # status. In this case $! is set to 0
        $plugin->nagios_exit( UNKNOWN,
            "Error while closing pipe to $which_command: $OS_ERROR" );
    }

    return $path;

}

##############################################################################
# main
#

################################################################################
# Initialization

$status     = 0;
$status_msg = q{};
$verbosity  = 0;

$plugin = Nagios::Plugin->new( shortname => 'CHECK_CONNECTIONS' );

########################
# Command line arguments

$options = Nagios::Plugin::Getopt->new(
    usage   => 'Usage: %s [--help] [--verbose] [--version] [--timeout t]',
    version => $VERSION,
    url     => 'https://trac.id.ethz.ch/projects/nagios_plugins',
    blurb   => 'monitors the number open TCP connections',
);

$options->arg(
    spec     => 'critical|c=i',
    help     => 'connection limit for a critical warning',
    required => 1,
);

$options->arg(
    spec     => 'warning|w=i',
    help     => 'connection limit for a warning',
    required => 1,
);

$options->arg(
    spec     => 'netstat=s',
    help     => 'path of the netstat utility',
    required => 0,
);

$options->getopts();

###############
# Sanity checks

if ( !check_positive_integer( $options->critical ) || $options->critical <= 0 )
{
    $plugin->nagios_exit( UNKNOWN, 'unable to parse critical' );
}

if ( !check_positive_integer( $options->warning ) || $options->warning <= 0 ) {
    $plugin->nagios_exit( UNKNOWN, 'unable to parse warning' );
}

if ( $options->critical < $options->warning ) {
    $plugin->nagios_exit( UNKNOWN,
        'critical has to be greater or equal warning' );
}

$netstat = $options->netstat;

if ( !$netstat ) {
    $netstat = get_path('netstat');
}

if ( !$netstat ) {
    $plugin->nagios_exit( UNKNOWN, 'Unable to find the "netstat" utility"' );
}

if ( !-x $netstat ) {
    $plugin->nagios_exit( UNKNOWN, "$netstat is not executable" );
}

alarm $options->timeout;

verbose "using $netstat\n", 2;

################
# Set the limits

$threshold = Nagios::Plugin::Threshold->set_thresholds(
    warning  => $options->warning,
    critical => $options->critical,
);

################################################################################

$command = "$netstat -an";

verbose qq{Executing "$command"\n};

$pid = open $output, q{-|}, "$command 2>&1"
  or $plugin->nagios_exit( UNKNOWN, "Cannot execute $command: $OS_ERROR" );

# read the whole file
@lines = read_file($output);

if ( $verbosity > 2 ) {
    for my $line (@lines) {
        verbose "$line", 1;
    }
}

# skip the first two lines (header)
shift @lines;
shift @lines;

# continue to parse until we detect a known protocol
#   TCP
#   UTP
#   ICMP

$tcp     = 0;
$udp     = 0;
$udp_in  = 0;
$icmp    = 0;
$icmp_in = 0;

initialize_state_table();

for my $line (@lines) {

    if ( $line =~ /^tcp/mxs ) {

        $tcp++;

        ( $protocol, $recv, $send, $local, $remote, $state ) = split /\s+/mxs,
          $line;

        $state = standardize_state($state);

        if ( !defined $states{$state} ) {
            $plugin->nagios_exit( UNKNOWN, "unknown TCP state '$state'" );
        }

        $states{$state}++;

    }
    elsif ( $line =~ /^udp/mxs ) {

        ( $protocol, $recv, $send, $local, $remote ) = split /\s+/mxs, $line;

        if ( $remote eq '*.*' ) {
            $udp_in++;
        }

        $udp++;
    }
    elsif ( $line =~ /^icm/mxs ) {

        ( $protocol, $recv, $send, $local, $remote ) = split /\s+/mxs, $line;

        if ( $remote eq '*.*' ) {
            $icmp_in++;
        }

        $icmp++;

    }
    else {
        last;
    }

}

if (  !( close $output )
    && ( $OS_ERROR != 0 ) )
{

    # close to a piped open return false if the command with non-zero
    # status. In this case $! is set to 0
    $plugin->nagios_exit( UNKNOWN,
        "Error while closing pipe to $command: $OS_ERROR" );
}

$total = $udp + $tcp + $icmp;

for my $state ( keys %states ) {

    $plugin->add_perfdata(
        label => "$state",
        value => $states{$state},
        uom   => q{},
    );

}

$plugin->add_perfdata(
    label => 'TOTAL',
    value => $total,
    uom   => q{},
);

$plugin->add_perfdata(
    label => 'TCP',
    value => $tcp,
    uom   => q{},
);

$plugin->add_perfdata(
    label => 'UDP',
    value => $udp,
    uom   => q{},
);

$plugin->add_perfdata(
    label => 'UDP_LISTEN',
    value => $udp_in,
    uom   => q{},
);

$plugin->add_perfdata(
    label => 'ICMP',
    value => $icmp,
    uom   => q{},
);

$plugin->add_perfdata(
    label => 'ICMP_LISTEN',
    value => $icmp_in,
    uom   => q{},
);

$plugin->nagios_exit( $threshold->get_status($total), "$total connections" );

1;
