bumped version to 2.0.1
[fwknop.git] / extras / fwknop-launcher / fwknop-launcher-lsof.pl
1 #!/usr/bin/perl -w
2 #
3 #############################################################################
4 #
5 # File: fwknop-launcher-lsof.pl
6 #
7 # URL: http://www.cipherdyne.org/fwknop/
8 #
9 # Purpose:  This script provides a lightweight mechanism to launch the fwknop
10 #           client shortly after a local user tries to initiate a connection
11 #           to a remote service that is protected by fwknopd.  The advantage
12 #           of using this script is that local users don't have to run the
13 #           fwknop client themselves before initiating an outbound
14 #           connection.  The idea for this script came from Sebastien
15 #           Jeanquier.  It is not required to have the fwknopd daemon
16 #           installed in order for this script to be effective - only the
17 #           fwknop client needs to be available.
18 #
19 # Author: Michael Rash (mbr@cipherdyne.org)
20 #
21 # Version: 2.0.1
22 #
23 # Copyright (C) 2011 Michael Rash (mbr@cipherdyne.org)
24 #
25 # License - GNU Public License version 2 (GPLv2):
26 #
27 #    This program is distributed in the hope that it will be useful,
28 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
29 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
30 #    GNU General Public License for more details.
31 #
32 #    You should have received a copy of the GNU General Public License
33 #    along with this program; if not, write to the Free Software
34 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
35 #    USA
36 #
37 #############################################################################
38 #
39
40 use POSIX;
41 use Getopt::Long;
42 use strict;
43
44 #================== config ==================
45 my $launcher_config = '~/.fwknop-launcher.conf';
46
47 my $sleep_interval = 1;
48
49 my $lsof_cmd   = '/usr/bin/lsof';
50 my $fwknop_cmd = '/usr/bin/fwknop';
51 #================ end config ================
52
53 my $ip_re = qr|(?:[0-2]?\d{1,2}\.){3}[0-2]?\d{1,2}|;  ### IPv4
54
55 my %spa_dispatch_cache = ();
56 my %spa_config_dsts    = ();
57 my $fwknop_args_append   = '';
58 my $fwknop_args_override = '';
59 my $key_file  = '';
60 my $home_dir  = '';
61 my $user      = '';
62 my $no_daemon = 0;
63 my $verbose   = 0;
64 my $help      = 0;
65
66 die "[*] Use --help for usage information.\n" unless(GetOptions (
67     'config=s'         => \$launcher_config, # launcher config
68     'lsof-cmd=s'       => \$lsof_cmd,        # lsof path
69     'fwknop-cmd=s'     => \$fwknop_cmd,      # fwknop path
70     'sleep-interval=i' => \$sleep_interval,  # seconds
71     'user=s'           => \$user,            # set the user
72     'home-dir=s'       => \$home_dir,        # set the home dir
73     'no-daemon'        => \$no_daemon,       # run in the foreground
74     'verbose'          => \$verbose,         # verbose mode
75     'help'             => \$help             # Print help
76 ));
77
78 &usage() if $help;
79
80 &init();
81
82 &daemonize() unless $no_daemon;
83
84 &watch_lsof();
85
86 exit 0;
87
88 #============== end main =============
89
90 sub watch_lsof() {
91
92     my $lsof_exec_ctr = 0;
93
94     for (;;) {
95
96         my $cmd = "$lsof_cmd -u $user -a -n -P -i 4";  ### IPv4
97
98         print "[+] Executing: $cmd\n" if $no_daemon and $verbose;
99
100         open LSOF, "$cmd |" or die "[*] Could not execute $cmd: $!";
101
102         ### COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
103         ### telnet  31483  mbr    3u  IPv4  54543      0t0  TCP 127.0.0.1:41619->127.0.0.1:1234 (SYN_SENT)
104
105         while (<LSOF>) {
106             if (/^(\S+)\s+(\d+)\s+\S+\s+.*\s(\S+)\s+($ip_re):
107                     (\d+)\-\>($ip_re):(\d+)\s+\(SYN_SENT\)/x) {
108                 my $command = $1;
109                 my $pid     = $2;
110                 my $proto   = lc($3);
111                 my $src_ip  = $4;
112                 my $s_port  = $5;
113                 my $dst_ip  = $6;
114                 my $d_port  = $7;
115
116                 print "[+] Matched line: $_" if $no_daemon and $verbose;
117
118                 my $connection_string = "$command:$proto:$src_ip:$s_port:$dst_ip:$d_port";
119
120                 next if defined $spa_dispatch_cache{$connection_string};
121
122                 if (&is_valid_fwknop_dst($command, $proto, $dst_ip, $d_port)) {
123
124                     if ($no_daemon and $verbose) {
125                         print "[+] Matched connection, launching fwknop.\n";
126                     }
127
128                     &dispatch_fwknop($proto, $dst_ip, $d_port);
129
130                     ### add this line to the dispatch cache so that we don't
131                     ### send multiple SPA packets for the same connection
132                     $spa_dispatch_cache{$connection_string} = '';
133
134                 } else {
135                     if ($no_daemon and $verbose) {
136                         print "[-] Attempted connection not matched by ",
137                             "any SPA_ACCESS variable in $launcher_config\n";
138                     }
139                 }
140             }
141         }
142         close LSOF;
143
144         $lsof_exec_ctr++;
145
146         if ($lsof_exec_ctr == 3600) {
147             %spa_dispatch_cache = ();
148             $lsof_exec_ctr = 0;
149         }
150
151         sleep $sleep_interval;
152     }
153     return;
154 }
155
156 sub is_valid_fwknop_dst() {
157     my ($cmd, $proto, $dst_ip, $d_port) = @_;
158
159     CMD: for my $access_cmd (keys %spa_config_dsts) {
160         next CMD unless $access_cmd eq 'any' or $cmd eq $access_cmd;
161         PROTO: for my $access_proto (keys %{$spa_config_dsts{$access_cmd}}) {
162             next PROTO unless $access_proto eq 'any' or $proto eq $access_proto;
163             IP: for my $access_ip (keys %{$spa_config_dsts{$access_cmd}{$access_proto}}) {
164                 next IP unless $access_ip eq 'any' or $access_ip eq $dst_ip;
165                 PORT: for my $access_port
166                         (keys %{$spa_config_dsts{$access_cmd}{$access_proto}{$access_ip}}) {
167                     return 1 if $access_port eq 'any' or $d_port == $access_port;
168                 }
169             }
170         }
171     }
172
173     return 0;
174 }
175
176 sub dispatch_fwknop() {
177     my ($proto, $dst_ip, $d_port) = @_;
178
179     my $fwknop_cmd_str = "$fwknop_cmd -A $proto/$d_port -s -D " .
180             "$dst_ip --get-key $key_file";
181
182     if ($fwknop_args_append) {
183         $fwknop_cmd_str .= " $fwknop_args_append";
184     } elsif ($fwknop_args_override) {
185         $fwknop_cmd_str = "$fwknop_cmd $fwknop_args_override";
186     }
187
188     print "[+] Executing: $fwknop_cmd_str\n" if $no_daemon;
189
190     open FWKNOP, "$fwknop_cmd_str |"
191         or die "[*] Could not execute: $fwknop_cmd_str: $!";
192     close FWKNOP;
193
194     return 0;
195 }
196
197 sub daemonize() {
198
199     my $pid = fork();
200     exit 0 if $pid;
201     die "[*] $0: Couldn't fork: $!" unless defined $pid;
202     POSIX::setsid() or die "[*] $0: Can't start a new session: $!";
203
204     return;
205 }
206
207 sub init() {
208
209     if ($launcher_config =~ m|^\~|) {
210
211         ### get the path to the user's home directory
212         &get_homedir() unless $home_dir;
213
214         $launcher_config =~ s|^\~|$home_dir|;
215     }
216
217     my $found_key_file = 0;
218     my $found_access_var = 0;
219
220     my $line_ctr = 1;
221     open F, "< $launcher_config"
222         or die "[*] Could not open $launcher_config: $!";
223     while (<F>) {
224         if (/^SPA_ACCESS\s+(\S+):(\S+):(\S+):(\S+);/) {
225             my $cmd    = $1;
226             my $proto  = lc($2);
227             my $dst_ip = $3;
228             my $d_port = $4;
229
230             ### validate the protocol
231             unless ($proto eq 'any'
232                 or $proto eq 'tcp'
233                 or $proto eq 'udp'
234             ) {
235                 die "[*] Invalid proto '$proto' ",
236                     "in $launcher_config at line: $line_ctr";
237             }
238
239             ### validate connection destination IP
240             unless ($dst_ip eq 'any'
241                     or $dst_ip =~ /$ip_re/) {
242                 die "[*] Invalid IP '$dst_ip' ",
243                     "in $launcher_config at line: $line_ctr";
244             }
245
246             ### validate connection destination port
247             unless ($d_port eq 'any'
248                     or $d_port =~ /^\d+$/) {
249                 die "[*] Invalid port '$d_port' ",
250                     "in $launcher_config at line: $line_ctr";
251             }
252
253             ### add this dst into the valid destinations cache
254             $spa_config_dsts{$cmd}{$proto}{$dst_ip}{$d_port} = '';
255
256             $found_access_var = 1;
257
258         } elsif (/^KEY_FILE\s+(\S+);/) {
259             $key_file = $1;
260
261             if ($key_file =~ m|^\~|) {
262                 &get_homedir() unless $home_dir;
263                 $key_file =~ s|^\~|$home_dir|;
264             }
265
266             $found_key_file = 1;
267
268         } elsif (/^USER\s+(\S+);/) {
269             ### might have been set via the command line
270             $user = $1 unless $user;
271
272         } elsif (/^FWKNOP_ARGS_APPEND\s+\"(.*)\";/) {
273             $fwknop_args_append = $1;
274         } elsif (/^FWKNOP_ARGS_OVERRIDE\s+\"(.*)\";/) {
275             $fwknop_args_override = $1;
276         }
277
278         $line_ctr++;
279     }
280     close F;
281
282     unless ($found_key_file) {
283         die "[*] Must have a key file defined via KEY_FILE";
284     }
285
286     unless ($found_access_var) {
287         die "[*] Must have at least one SPA_ACCESS var defined";
288     }
289
290     &get_username() unless $user;
291
292     return;
293 }
294
295 sub get_homedir() {
296
297     $home_dir = (getpwuid($<))[7];
298
299     unless ($home_dir) {
300         $home_dir = $ENV{'HOME'} if defined $ENV{'HOME'};
301     }
302
303     die "[*] Could not determine home directory. Use the -d <homedir> option."
304         unless $home_dir;
305
306     return;
307 }
308
309 sub get_username() {
310
311     $user = (getpwuid($<))[0];
312
313     unless ($user) {
314         if (defined $ENV{'USER'}) {
315             $user = $ENV{'USER'};
316         } elsif (defined $ENV{'USERNAME'}) {
317             $user = $ENV{'USERNAME'};
318         }
319     }
320
321     die "[*] Could not determine username. Use the -u <user> option."
322         unless $user;
323
324     return;
325 }
326
327 sub usage() {
328     print <<_HELP_;
329
330 Usage: fwknop-launcher-lsof.pl [options]
331
332 Options:
333
334     -c,  --config     <file>   - Path to fwknop-launcher.conf config file.
335     -l,  --lsof-cmd   <path>   - Path to lsof command.
336     -f,  --fwknop-cmd <path>   - Path to fwknop client command.
337     -s,  --sleep   <seconds>   - Specify sleep interval (default:
338                                  $sleep_interval seconds)
339     -n   --no-daemon           - Run in foreground mode.
340     -u,  --user   <username>   - Specify username (usually this is not
341                                  needed).
342          --home-dir <dir>      - Path to user's home directory (usually
343                                  this is not needed).
344     -v   --verbose             - Print verbose information to the terminal
345                                  (requires --no-daemon).
346          --help                - Print usage info and exit.
347
348 _HELP_
349     exit 0;
350 }