Subversion Repositories sysadmin_scripts

Rev

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