Subversion Repositories sysadmin_scripts

Rev

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