Subversion Repositories sysadmin_scripts

Rev

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