Subversion Repositories sysadmin_scripts

Rev

Rev 61 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

#! /usr/bin/perl -w

#    vpn - Manages OpenVPN sessions
#    Copyright (C) 2016  R. W. Rodolico
#
#    This program 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.
#
#    This program 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.
#
#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#    HISTORY:
#    v0.1 - 20160311 RWR
#       Initial Release
#    v0.2 - 20160312 RWR
#       Added --chdir parameter to allow relative processing of files
#             from the .ovpn config file
#       Added --version parameter to display version information
#       Created copyright using GNUv2


$main::VERSION = '0.2';


use Getopt::Long qw(:config auto_version bundling );
use Pod::Usage qw(pod2usage);

my $configDirs = '/etc/openvpn';
my $logDir = '/var/log/openvpn';
my $pidDir = '/var/run/openvpn';
my $statusDir = '/var/run/openvpn';
my $timeOut = 60 * 60; # number of seconds of inactivity to close session

# These variables are for getOpt, and control the operation of the script
# I left them all global
my $kill = '';
my $show = '';
my $destination = '';
my $quiet = '';
my $verbose = '';
my $help = 0;
my $man = 0;
my $chdir = 0;


# check if Directories exist and, if root, creates them if needed 
sub validateDirectories {
   my @errors;
   foreach my $dir ( $logDir, $pidDir, $statusDir ) {
      if ( ! -d $dir ) {
         if ( $< ) {
            push @errors,$dir;
         } else {
            `sudo mkdir -p $dir`;
         }
      }
   }
   if ( @errors ) {
      die "The following directories do not exist, rerun as root user\n\t" .
          join( "\n\t", @errors ) . "\n";
   } 
}

# get a pid from a session name, and verify it with a ps
# returns the PID, or an empty string if not found
# SIDE EFFECT: Will remove the .pid file if the process is not
# actually running
sub getPid {
   my $sessionName = shift;
   my $pidFile = "$pidDir/$sessionName.pid";
   my $pid = '';
   if ( -f $pidFile && open PID,"<$pidFile" ) {
      $pid = <PID>;
      close PID;
      chomp $pid;
      print "Found pid file " if $verbose;
      print "\tChecking if pid $pid exists\n" if $verbose;
      if ( `ps --pid $pid --no-headers -o pid` ) {
         print "\tFound PID $pid\n" if $verbose;
         return $pid;
      } else {
         if ( $< ) {
            print STDERR "Invalid PID file $pidDir/$sessionName.pid but can not remove as non-root user\n";
         } else {
            print STDERR "Invalid PID file $pidDir/$sessionName.pid removed\n" unless $quiet;
            `rm "$pidDir/$sessionName.pid"`;
         }
         return '';
      }
   }
   return $pid;
}
   
# get all available sessions and their status
# returns them in a hash
sub getSessions {
   my %sessions;
   my @possibleSessions = `ls $configDirs`;
   my @active;
   chomp @possibleSessions;
   @possibleSessions = grep{ -d "$configDirs/$_" } @possibleSessions;
   foreach my $thisSession ( @possibleSessions ) {
      if ( $pid = &getPid( $thisSession ) ) {
         $sessions{$thisSession}{'pidFile'} = "$pidDir/$thisSession.pid";
         $sessions{$thisSession}{'logFile'} = "$logDir/$thisSession.log";
         $sessions{$thisSession}{'statusFile'} = "$statusDir/$thisSession.status";
         $sessions{$thisSession}{'pid'} = $pid;
      } else {
         $sessions{$thisSession}{'pid'} = 0;
      }
   }
   return \%sessions;
}


# displays all available sessions and their status
sub printSessions {
   my $sessions = &getSessions();
   print '-'x40 . "\nActive\tSession\t\tPID\n";
   foreach my $session ( sort keys %$sessions ) {
      print $$sessions{$session}{'pid'} ? "*" : " ";
      print "\t$session" . ' 'x (15 - length( $session ));
      if ( $$sessions{$session}{'pid'} ) {
         print "\t" . $$sessions{$session}{'pid'};
      }
      print "\n";
   }
   print '-'x40 . "\n";
   print "Status files located in $statusDir\n" if $verbose;
   print "Log Files located in $logDir\n" if $verbose;
   print "PID files located in $pidDir\n" if $verbose;
}

# start a connection. Can only be done as root user.
sub startConnection {
   my $destination = shift;
   my $configFile = "$configDirs/$destination/$destination.ovpn";
   chdir( "$configDirs/$destination" ) if $chdir;
   if ( -f $configFile && ! &getPid($destination) ) {
      my $command = 'openvpn' .
                    " --daemon $destination" .
                    " --inactive $timeOut" .
                    " --writepid $pidDir/$destination.pid" .
                    " --log $logDir/$destination.log" .
                    " --status $statusDir/$destination.status" .
                    " --config $configFile";
      print "$command\n" if $verbose;
      system ( $command );
      if ( &getPid ( $destination ) ) {
         return "$destination is active";
      } else {
         return "There was a failure in the command, check $logDir/$destination.log\nCommand was\n$command";
      }
   } else {
      return "Could not open '$configFile'";
   }
   return "The connection is already active";
}

# kill all active connections
sub killALL {
   my $sessions = &getSessions();
   foreach my $session ( keys %$sessions ) {
      if ( $$sessions{$session}{'pid'} ) {
         $status = &killConnection( $session );
         print "$status\n" unless $quiet;
      } # if
   } # foreach
} # killAll
      
                                       
# kills a connection
sub killConnection {
   my $connection = shift;
   my $pid = &getPid( $connection );
   if ( $pid ) {
      `kill $pid`;
      `rm "$pidDir/$connection.pid"`;
      return "Session $connection killed and pidfile removed\n";
   } else {
      return "$connection not running\n";
   }
}

#### some housekeeping
&validateDirectories(); # check if directories exist and, if root, create them if needed

# process options
GetOptions( 
   'kill|k=s' => \$kill, 
   'display|d' => \$show, 
   'start|s=s' => \$destination, 
   'timeout|t=i' => \$timeOut,
   'quiet|q' => \$quiet,
   'chdir|c' => \$chdir,
   'verbose|v' => \$verbose,
   'help|?' => \$help,
   'man' => \$man
);

pod2usage(1) if $help;
pod2usage( -exitval => 0, -verbose => 2 ) if $man;

# process rest of command line if it is there (name of connection)
$destination = shift if @ARGV > 0;
$show = 1 unless $destination || $kill; # if no destination given, default to show

#### main program

if ( $kill ) {
   die "Kill requires you to be root, use sudo\n" if $<;
   $status = ( $kill eq 'ALL' ) ? &killALL() : &killConnection( $kill );
   print "$status\n" unless $quiet;
} elsif ( $destination ) {
   die "Start requires you to be root, use sudo\n" if $<;
   my $status =  &startConnection( $destination ) unless &getPid( $destination );
   print "$status\n" unless $quiet;
}

&printSessions() if $show;

1;

__END__

=head1 NAME

vpn

=head1 SYNOPSIS

  vpn             Show status of all available sessions
  vpn session     Start a session (must be root)
  vpn [options]

Controls a set of OpenVPN connections, starting, stopping, and auto-timeouts.
   
=head1 OPTIONS

=over 3

=item B<--kill|-k> I<session>

Kill the named session if running. The keyword ALL (case sensitive) will kill all running sessions

=item B<--display|-d>

Display all available sessions and their current status

=item B<--destination> I<session>

Work with a particular destination

=item B<--start|-ss> I<session>

Start a session. Will check if session already running and not attempt a second connection.

=item B<--timeout|-t> I<seconds>

Set idle timeout, in seconds

=item B<--version>

Display version information

=item B<--chdir|-c>

Causes a chdir to be run before the actual openvpn command is executed. Useful if your pkcs12 file entry does not have a fully qualified path.

=item B<--verbose|-v>

Shows some extra information while processing.

=item B<--help|-?>

This screen

=item B<--man>

Prints the full man page

=back

=head1 DESCRIPTION

Each possible session is assumed to be stored in subdirectories of
$configDirs, with a configuration file of the same name as the subdirectory
and a .ovpn suffix. Any paths in the configuration file must be fully qualified
(ie pkcs12, etc...).

For example
   $configDirs
      +--- vpn1
      |  +--- vpn1.ovpn
      |  |
      |  +--- other files (such as pkcs12)
      |
      +--- vpn2
         +--- vpn2.ovpn
         |
         +--- other files (such as pkcs12)

Based on this, the command vpn vpn2 would look in $configDirs for vpn2.ovpn, then run openvpn using that configuration file.

B<NOTE>: if the configuration file does not have the fully qualified path to any files used (such as pkcs12 files), it will not be able to use them. You can modify this with the -chdir option, which will move into the directory before calling openvpn

The script will then create several files

=over 3

=item B<Log File> .log

Created in $logDir (default /var/log/openvpn). In this case, it would be /var/log/openvpn/vpn1.log

=item B<Status File> .status

Created in $statusDir (default /var/run/openvpn). In this case, would be /var/run/openvpn/vpn1.status

=item B<PID File> .pid

Created in $pidDir (default /var/run/openvpn). In this case would be /var/run/openvpn/vpn1.pid

=back

The Log and Status files are recreated each time a session is started (ie, stopping and starting vpn1 would overwrite the old files). The Pid file is automatically removed when a session is killed.


=head1 CAVEATS

Sometimes, the pid files can become out of sync with reality, especially with a reboot. When --show or --kill ALL are called, a cleanup on these files is done. You can, as a part of reboot, safely call vpn with the --kill ALL function.

Most of the options require elevated privileges as openvpn creates virtual devices on the system. While vpn --show can display the status of all possible sessions, it will complain if there is an old session file (.pid) it can not remove. It will still show the sessions though. Either manually remove the file, or run again with elevated privileges.

The script checks for the existance of the required directories (log, pid and status) and will attempt to create them if they don't exist. If you are not running with elevated privileges, it will complain, then exit.