Subversion Repositories sysadmin_scripts

Rev

Rev 148 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
23 rodolico 1
#! /usr/bin/perl -w
2
 
3
#    vpn - Manages OpenVPN sessions
4
#    Copyright (C) 2016  R. W. Rodolico
5
#
6
#    This program is free software: you can redistribute it and/or modify
7
#    it under the terms of the GNU General Public License as published by
8
#    the Free Software Foundation, either version 2 of the License, or
9
#    (at your option) any later version.
10
#
11
#    This program is distributed in the hope that it will be useful,
12
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
13
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
#    GNU General Public License for more details.
15
#
16
#    You should have received a copy of the GNU General Public License
17
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
#
19
#    HISTORY:
20
#    v0.1 - 20160311 RWR
21
#       Initial Release
22
#    v0.2 - 20160312 RWR
23
#       Added --chdir parameter to allow relative processing of files
24
#             from the .ovpn config file
25
#       Added --version parameter to display version information
26
#       Created copyright using GNUv2
61 rodolico 27
#    v0.2.1 - 20191209 RWR
28
#       Since the openvpn cli does not return any exit codes (always 0), set it to monitor
29
#       the logs, looking for. See sub verifyUp for details.
23 rodolico 30
 
148 rodolico 31
# On secure systems, you can set this up with the setuid bit to run as root
32
# chown root:root vpn
33
# chmod u+s vpn
23 rodolico 34
 
148 rodolico 35
 
61 rodolico 36
$main::VERSION = '0.2.1';
23 rodolico 37
 
38
 
39
use Getopt::Long qw(:config auto_version bundling );
40
use Pod::Usage qw(pod2usage);
41
 
42
my $configDirs = '/etc/openvpn';
43
my $logDir = '/var/log/openvpn';
44
my $pidDir = '/var/run/openvpn';
45
my $statusDir = '/var/run/openvpn';
46
my $timeOut = 60 * 60; # number of seconds of inactivity to close session
47
 
48
# These variables are for getOpt, and control the operation of the script
49
# I left them all global
50
my $kill = '';
51
my $show = '';
52
my $destination = '';
53
my $quiet = '';
54
my $verbose = '';
55
my $help = 0;
56
my $man = 0;
57
my $chdir = 0;
58
 
59
 
60
# check if Directories exist and, if root, creates them if needed 
61
sub validateDirectories {
62
   my @errors;
63
   foreach my $dir ( $logDir, $pidDir, $statusDir ) {
64
      if ( ! -d $dir ) {
65
         if ( $< ) {
66
            push @errors,$dir;
67
         } else {
68
            `sudo mkdir -p $dir`;
69
         }
70
      }
71
   }
72
   if ( @errors ) {
73
      die "The following directories do not exist, rerun as root user\n\t" .
74
          join( "\n\t", @errors ) . "\n";
75
   } 
76
}
77
 
78
# get a pid from a session name, and verify it with a ps
79
# returns the PID, or an empty string if not found
80
# SIDE EFFECT: Will remove the .pid file if the process is not
81
# actually running
82
sub getPid {
83
   my $sessionName = shift;
84
   my $pidFile = "$pidDir/$sessionName.pid";
85
   my $pid = '';
86
   if ( -f $pidFile && open PID,"<$pidFile" ) {
87
      $pid = <PID>;
88
      close PID;
89
      chomp $pid;
90
      print "Found pid file " if $verbose;
91
      print "\tChecking if pid $pid exists\n" if $verbose;
92
      if ( `ps --pid $pid --no-headers -o pid` ) {
93
         print "\tFound PID $pid\n" if $verbose;
94
         return $pid;
95
      } else {
96
         if ( $< ) {
97
            print STDERR "Invalid PID file $pidDir/$sessionName.pid but can not remove as non-root user\n";
98
         } else {
99
            print STDERR "Invalid PID file $pidDir/$sessionName.pid removed\n" unless $quiet;
100
            `rm "$pidDir/$sessionName.pid"`;
101
         }
102
         return '';
103
      }
104
   }
105
   return $pid;
106
}
107
 
108
# get all available sessions and their status
109
# returns them in a hash
110
sub getSessions {
111
   my %sessions;
112
   my @possibleSessions = `ls $configDirs`;
113
   my @active;
114
   chomp @possibleSessions;
115
   @possibleSessions = grep{ -d "$configDirs/$_" } @possibleSessions;
116
   foreach my $thisSession ( @possibleSessions ) {
117
      if ( $pid = &getPid( $thisSession ) ) {
118
         $sessions{$thisSession}{'pidFile'} = "$pidDir/$thisSession.pid";
119
         $sessions{$thisSession}{'logFile'} = "$logDir/$thisSession.log";
120
         $sessions{$thisSession}{'statusFile'} = "$statusDir/$thisSession.status";
121
         $sessions{$thisSession}{'pid'} = $pid;
122
      } else {
123
         $sessions{$thisSession}{'pid'} = 0;
124
      }
125
   }
126
   return \%sessions;
127
}
128
 
129
 
130
# displays all available sessions and their status
131
sub printSessions {
132
   my $sessions = &getSessions();
133
   print '-'x40 . "\nActive\tSession\t\tPID\n";
134
   foreach my $session ( sort keys %$sessions ) {
135
      print $$sessions{$session}{'pid'} ? "*" : " ";
148 rodolico 136
      print "\t$session" . ' 'x (length($session) < 25 ? 25 - length( $session ) : 25);
23 rodolico 137
      if ( $$sessions{$session}{'pid'} ) {
138
         print "\t" . $$sessions{$session}{'pid'};
139
      }
140
      print "\n";
141
   }
142
   print '-'x40 . "\n";
143
   print "Status files located in $statusDir\n" if $verbose;
144
   print "Log Files located in $logDir\n" if $verbose;
145
   print "PID files located in $pidDir\n" if $verbose;
146
}
147
 
61 rodolico 148
# simply returns the return code of a process
149
sub processReturnCode {
150
   my $code = shift;
151
   if ($code == -1) {
152
      return $code;
153
   }
154
   $code = $code >> 8;
155
   return ( $code );
156
}
157
 
158
# checks a log $count times, with a delay of $delay, for one of the messages below.
159
# The first two indicate failure, the last one indicates success.
160
sub verifyUp {
161
   my $logFile = shift;
162
   my $delay = 1;
163
   my $count = 10;
164
   my $returnCode;
165
   while ( $count-- ) {
166
      qx/grep 'AUTH_FAILED' $logFile 2>&1/;
167
      $returnCode = &processReturnCode( $? );
168
      print "auth failed grep returned [$returnCode]\n" if $verbose;
169
      return 0 if ( $returnCode ) == 0;
170
      qx/grep 'private key password verification failed' $logFile 2>&1/;
171
      $returnCode = &processReturnCode( $? );
172
      print "Checking private key password [$returnCode]\n" if $verbose;
173
      return 0 if ( $returnCode ) == 0;
174
 
175
      qx/grep 'Initialization Sequence Completed' $logFile 2>&1/;
176
      $returnCode = &processReturnCode( $? );
177
      print "initialization complete grep returned [$returnCode]\n" if $verbose;
178
      return 1 if ( $returnCode ) == 0;
179
      print "Sleeping for $delay seconds\n" if $verbose;
180
      sleep $delay;
181
   }
182
   return 0;
183
}
184
 
23 rodolico 185
# start a connection. Can only be done as root user.
186
sub startConnection {
187
   my $destination = shift;
61 rodolico 188
   my $exitString = 'Unknown Exit Status';
23 rodolico 189
   my $configFile = "$configDirs/$destination/$destination.ovpn";
61 rodolico 190
   my $p12 =  "$configDirs/$destination/$destination.p12";
23 rodolico 191
   chdir( "$configDirs/$destination" ) if $chdir;
61 rodolico 192
   if ( -f $configFile ) {
193
      # we found the config file
194
      if ( &getPid( $destination ) ) { # make sure it is not already running
195
         return 'The connection was already active';
196
      }
23 rodolico 197
      my $command = 'openvpn' .
61 rodolico 198
                    ( -f $p12 ? " --askpass" : '' ) .
23 rodolico 199
                    " --daemon $destination" .
200
                    " --inactive $timeOut" .
201
                    " --writepid $pidDir/$destination.pid" .
202
                    " --log $logDir/$destination.log" .
203
                    " --status $statusDir/$destination.status" .
204
                    " --config $configFile";
205
      print "$command\n" if $verbose;
61 rodolico 206
      # run the command.
23 rodolico 207
      system ( $command );
61 rodolico 208
      #  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
209
      my $status = &verifyUp( "$logDir/$destination.log" );
210
      if ( $status == 1 ) { # good run, so we just say it 
211
         return "$destination now active with PID " . &getPid( $destination );
23 rodolico 212
      } else {
213
         return "There was a failure in the command, check $logDir/$destination.log\nCommand was\n$command";
214
      }
215
   } else {
216
      return "Could not open '$configFile'";
217
   }
61 rodolico 218
   return "We should never reach this point in startConnection";
23 rodolico 219
}
220
 
221
# kill all active connections
222
sub killALL {
223
   my $sessions = &getSessions();
224
   foreach my $session ( keys %$sessions ) {
225
      if ( $$sessions{$session}{'pid'} ) {
226
         $status = &killConnection( $session );
227
         print "$status\n" unless $quiet;
228
      } # if
229
   } # foreach
230
} # killAll
231
 
232
 
233
# kills a connection
234
sub killConnection {
235
   my $connection = shift;
236
   my $pid = &getPid( $connection );
237
   if ( $pid ) {
238
      `kill $pid`;
239
      `rm "$pidDir/$connection.pid"`;
240
      return "Session $connection killed and pidfile removed\n";
241
   } else {
242
      return "$connection not running\n";
243
   }
244
}
245
 
246
#### some housekeeping
247
&validateDirectories(); # check if directories exist and, if root, create them if needed
248
 
249
# process options
250
GetOptions( 
251
   'kill|k=s' => \$kill, 
252
   'display|d' => \$show, 
253
   'start|s=s' => \$destination, 
254
   'timeout|t=i' => \$timeOut,
255
   'quiet|q' => \$quiet,
256
   'chdir|c' => \$chdir,
257
   'verbose|v' => \$verbose,
258
   'help|?' => \$help,
259
   'man' => \$man
260
);
261
 
262
pod2usage(1) if $help;
263
pod2usage( -exitval => 0, -verbose => 2 ) if $man;
264
 
265
# process rest of command line if it is there (name of connection)
266
$destination = shift if @ARGV > 0;
267
$show = 1 unless $destination || $kill; # if no destination given, default to show
268
 
269
#### main program
270
 
271
if ( $kill ) {
272
   die "Kill requires you to be root, use sudo\n" if $<;
273
   $status = ( $kill eq 'ALL' ) ? &killALL() : &killConnection( $kill );
274
   print "$status\n" unless $quiet;
275
} elsif ( $destination ) {
276
   die "Start requires you to be root, use sudo\n" if $<;
61 rodolico 277
   my $status =  &startConnection( $destination );
23 rodolico 278
   print "$status\n" unless $quiet;
279
}
280
 
281
&printSessions() if $show;
282
 
283
1;
284
 
285
__END__
286
 
287
=head1 NAME
288
 
289
vpn
290
 
291
=head1 SYNOPSIS
292
 
293
  vpn             Show status of all available sessions
294
  vpn session     Start a session (must be root)
295
  vpn [options]
296
 
297
Controls a set of OpenVPN connections, starting, stopping, and auto-timeouts.
298
 
299
=head1 OPTIONS
300
 
301
=over 3
302
 
303
=item B<--kill|-k> I<session>
304
 
305
Kill the named session if running. The keyword ALL (case sensitive) will kill all running sessions
306
 
307
=item B<--display|-d>
308
 
309
Display all available sessions and their current status
310
 
311
=item B<--destination> I<session>
312
 
313
Work with a particular destination
314
 
315
=item B<--start|-ss> I<session>
316
 
317
Start a session. Will check if session already running and not attempt a second connection.
318
 
319
=item B<--timeout|-t> I<seconds>
320
 
321
Set idle timeout, in seconds
322
 
323
=item B<--version>
324
 
325
Display version information
326
 
327
=item B<--chdir|-c>
328
 
329
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.
330
 
331
=item B<--verbose|-v>
332
 
333
Shows some extra information while processing.
334
 
335
=item B<--help|-?>
336
 
337
This screen
338
 
339
=item B<--man>
340
 
341
Prints the full man page
342
 
343
=back
344
 
345
=head1 DESCRIPTION
346
 
347
Each possible session is assumed to be stored in subdirectories of
348
$configDirs, with a configuration file of the same name as the subdirectory
349
and a .ovpn suffix. Any paths in the configuration file must be fully qualified
350
(ie pkcs12, etc...).
351
 
352
For example
353
   $configDirs
354
      +--- vpn1
355
      |  +--- vpn1.ovpn
356
      |  |
357
      |  +--- other files (such as pkcs12)
358
      |
359
      +--- vpn2
360
         +--- vpn2.ovpn
361
         |
362
         +--- other files (such as pkcs12)
363
 
364
Based on this, the command vpn vpn2 would look in $configDirs for vpn2.ovpn, then run openvpn using that configuration file.
365
 
366
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
367
 
368
The script will then create several files
369
 
370
=over 3
371
 
372
=item B<Log File> .log
373
 
374
Created in $logDir (default /var/log/openvpn). In this case, it would be /var/log/openvpn/vpn1.log
375
 
376
=item B<Status File> .status
377
 
378
Created in $statusDir (default /var/run/openvpn). In this case, would be /var/run/openvpn/vpn1.status
379
 
380
=item B<PID File> .pid
381
 
382
Created in $pidDir (default /var/run/openvpn). In this case would be /var/run/openvpn/vpn1.pid
383
 
384
=back
385
 
386
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.
387
 
388
 
389
=head1 CAVEATS
390
 
391
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.
392
 
393
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.
394
 
395
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.
396
 
397