Rev 23 | Rev 148 | 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
# v0.2.1 - 20191209 RWR
# Since the openvpn cli does not return any exit codes (always 0), set it to monitor
# the logs, looking for. See sub verifyUp for details.
$main::VERSION = '0.2.1';
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;
}
# simply returns the return code of a process
sub processReturnCode {
my $code = shift;
if ($code == -1) {
return $code;
}
$code = $code >> 8;
return ( $code );
}
# checks a log $count times, with a delay of $delay, for one of the messages below.
# The first two indicate failure, the last one indicates success.
sub verifyUp {
my $logFile = shift;
my $delay = 1;
my $count = 10;
my $returnCode;
while ( $count-- ) {
qx/grep 'AUTH_FAILED' $logFile 2>&1/;
$returnCode = &processReturnCode( $? );
print "auth failed grep returned [$returnCode]\n" if $verbose;
return 0 if ( $returnCode ) == 0;
qx/grep 'private key password verification failed' $logFile 2>&1/;
$returnCode = &processReturnCode( $? );
print "Checking private key password [$returnCode]\n" if $verbose;
return 0 if ( $returnCode ) == 0;
qx/grep 'Initialization Sequence Completed' $logFile 2>&1/;
$returnCode = &processReturnCode( $? );
print "initialization complete grep returned [$returnCode]\n" if $verbose;
return 1 if ( $returnCode ) == 0;
print "Sleeping for $delay seconds\n" if $verbose;
sleep $delay;
}
return 0;
}
# start a connection. Can only be done as root user.
sub startConnection {
my $destination = shift;
my $exitString = 'Unknown Exit Status';
my $configFile = "$configDirs/$destination/$destination.ovpn";
my $p12 = "$configDirs/$destination/$destination.p12";
chdir( "$configDirs/$destination" ) if $chdir;
if ( -f $configFile ) {
# we found the config file
if ( &getPid( $destination ) ) { # make sure it is not already running
return 'The connection was already active';
}
my $command = 'openvpn' .
( -f $p12 ? " --askpass" : '' ) .
" --daemon $destination" .
" --inactive $timeOut" .
" --writepid $pidDir/$destination.pid" .
" --log $logDir/$destination.log" .
" --status $statusDir/$destination.status" .
" --config $configFile";
print "$command\n" if $verbose;
# run the command.
system ( $command );
# openvpn always appears to have a return code of 0, so we need to look at the logs to see if we had success or failure
my $status = &verifyUp( "$logDir/$destination.log" );
if ( $status == 1 ) { # good run, so we just say it
return "$destination now active with PID " . &getPid( $destination );
} else {
return "There was a failure in the command, check $logDir/$destination.log\nCommand was\n$command";
}
} else {
return "Could not open '$configFile'";
}
return "We should never reach this point in startConnection";
}
# 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 );
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.