#!/usr/bin/perl # Encrypting VPN tunnel for use with userv-ipif. # # This comment is reference documentation. See ipif/INSTALL for the # installation instructions and setup tutorial. # # usage: # To make a tunnel between two machines `alice' and `bob', # on `alice', the active endpoint, run: # # udptunnel # [ -l[] ... . # | -e [/...] # | -m (`masquerade support': bob gets `Wait' instead of our addr/port) # | -d (`dump keys': when no peer, spew keys rather than reading them; # we always send keys to our peer if there is one) # | -Dcrypto (debug crypto - use with care, prints keys, packets &c on screen!) # | -f # ... # ] # , # , # ,,, # ,[,] # # [ [ ...] ] # # This will run udptunnel-forwarder on alice, and use # (usually an ssh invocation) to run udptunnel appropriately on bob. # Key material will be generated by alice and fed to udptunnel on bob # via 's stdin, and the physical address and port on bob # will be (if so configured) returned via 's stdout. # # The tunnel will stay up until one of the subprocesses involved dies, # or the keepalive timeout expires. If you want the tunnel to remain # up permanently, you must arrange to invoke it repeatedly (eg, from # inittab). See INSTALL. # # may be slip or cslip # # will be the MTU of the tunnel interfaces; it is best if this # is enough smaller than the path MTU between the physical interfaces # that the encapsulated packets will fit without fragmentation. # # Any <..-addr> supplied to udptunnel may also be hostname; these will # all be looked up on alice and IP addresses passed to bob. # # The `local' physical address and ports (ie, alice's own details), # may have these special values: # `Any' choose one ourselves and do not print it (the port chosen # will be supplied to bob) # `Print' choose one ourselves and print both port and addr # (this is not usually useful specified directly; it's # used by udptunnel when it invokes itself on bob via # , to have its other self print the # relevant value. # # The `remote' physical address and port (ie, on alice, bob's details), # may also have the special values: # `Command' wait for to tell us the values (this is # usually the right choice on alice for at least the # port). must be specified (ie, this # only makes sense on alice). # `Wait' alice will wait to receive a packet from bob and use # whatever address and port it came from # # These special values are case-sensitive. When alice runs udptunnel # on bob they are automatically translated to appropriate other values # in the arguments to bob's udptunnel. # # If is specified it should run udptunnel at the # bob end; it will be invoked as # [ ... ] # [ <-e arguments passed along> ] # , # , # ,,, # ,[,] # # # If it was given Print for , udptunnel's first stdout # output will be the real , pair. It # may then produce more stdout which, if any, will be forwarded to the # local end's stdout as debugging info. # # After this, if any encryption was specified, the encryption key # material will be fed into its stdin. See the documentation in the # mech-*.c files for details of the parameters. udptunnel on alice # will arrange to feed the keys fd of udptunnel-forwarder into the # stdin of the udptunnel on bob. # # is as follows: # # actual addr/port that addr/port # `Command' `Print' # `Wait' `Any' # # is as follows: # # (-m not specified) (-m specified) # actual addr/port that addr/port `Wait' # `Print' the chosen address `Wait' # `Any' `Wait' for addr, `Wait' # chosen port for port # # In each case udptunnel will run userv ipif locally, as # userv root ipif ,,, # # or, if -l was given, userv root ipif is replaced with the argument(s) # following -l option(s) until `.'. # # udptunnel will also run udptunnel-forwarder with appropriate options. # # recommended encryption parameters are: # -e nonce (prepend 32 bit counter) # -e timestamp// (prepend 32 bit time_t, and check on receipt) # -e pkcs5/8 (pad as per PKCS#5 to 8-byte boundary) # -e blowfish-cbcmac/128 (prepend CBC MAC with random IV and 128 bit key) # -e blowfish-cbc/128 (encrypt with CBC, random IV and 128 bit key) # where is perhaps 10 and perhaps 30. If your # clocks are not sufficiently well synchronised, you could replace # `-e nonce -e timestamp/...' with just `-e sequence'. Do not just # remove `-e timestamp/...'. # Copyright 1996-2013 Ian Jackson # Copyright 1998 David Damerell # Copyright 1999,2003 # Chancellor Masters and Scholars of the University of Cambridge # Copyright 2010 Tony Finch # # This is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with userv-utils; if not, see http://www.gnu.org/licenses/. use Socket; use POSIX; use Fcntl; $progname= $0; $progname =~ s,.*/,,; $|=1; chomp($hostname= `uname -n`); $? and die "$progname: cannot get hostname (uname failed with code $?)\n"; sub quit ($) { die "$progname - $hostname: fatal error: $_[0]\n"; } sub debug ($) { print "$progname - $hostname: debug: $_[0]\n"; } sub fail ($) { quit("unexpected system call failure: $_[0]: $!"); } sub warning ($) { warn "$progname - $hostname: $_[0]\n"; } sub eat_addr_port ($) { my ($x) = @_; @ARGV or quit(", missing"); $_= shift(@ARGV); (m/^$x,/i && m/^[a-z]/ || m/,$x$/i && m/,[a-z]/) and warning("$_: use Mixed Case for special values"); m/^([0-9a-z][0-9a-z-+.]+|$x)\,(\d+|$x)$/ or quit("$_: , bad syntax". (m/[A-Z]/ ? ' (use lowercase for hostnames)' : '')); return ($1,$2); } sub conv_host_addr ($) { my ($s,$r,@h) = @_; return INADDR_ANY() if $s =~ m/^[A-Z][a-z]/; return $r if defined($r= inet_aton($s)); @h= gethostbyname($s) or quit("$s: cannot get address"); $h[2] eq &AF_INET or quit("$s: address is not IPv4"); @h < 5 or quit("$s: name maps to no addresses"); $r= $h[4]; @h == 5 or warning("$s: name has several addresses, using ".inet_ntoa($r)); return $r; } sub conv_port_number ($) { my ($s,$r) = @_; return 0 if $s =~ m/^[A-Z][a-z]/; $r= $s+0; $r>0 && $r<65536 or quit("$s: port out of range"); return $r; } sub show_addr ($) { my ($s,@s) = @_; @s= unpack_sockaddr_in($s); return inet_ntoa($s[1]); } sub show_port ($) { my ($s,@s) = @_; @s= unpack_sockaddr_in($s); return $s[0]; } sub show_addr_port ($) { my ($s) = @_; return show_addr($s).','.show_port($s); } sub arg_value ($$) { my ($val,$opt) = @_; $_= '-'; return $val if length $val; @ARGV or quit("$opt needs value"); return shift @ARGV; } @lcmd= (); @encryption= (); $masq= 0; $dump= 0; $fcmd= 'udptunnel-forwarder'; $xfwdopts= ''; while ($ARGV[0] =~ m/^-/) { $_= shift @ARGV; last if m/^--?$/; while (!m/^-$/) { if (s/^-l//) { push @lcmd,$_ if length; while (@ARGV && ($_= shift @ARGV) ne '.') { push @lcmd, $_; } $_= '-' } elsif (s/^-f//) { $fcmd= arg_value($_,'-f'); } elsif (s/^-e//) { $encrarg= arg_value($_,'-e'); push @remoteopts, "-e$encrarg"; @thisencryption= split m#/#, $encrarg; $thisencryption[0] =~ s/^/\|/; push @encryption, @thisencryption; } elsif (s/^-m/-/) { $masq= 1; } elsif (s/^-d/-/) { $dump= 1; } elsif (s/^-Dcrypto$/-/) { $xfwdopts.= 'K'; push @remoteopts, '-Dcrypto'; } else { quit("unknown option \`$_'"); } } } # Variables \$[lr]a?p?(|s|d|r) # Local/Remote Address&/Port # actualvalue/Specified/Displaypassdown/fromRemote/passtoForwarder # ($las,$lps)= eat_addr_port('Print|Any'); $la= conv_host_addr($las); $lp= conv_port_number($lps); $ls= pack_sockaddr_in $lp,$la; ($ras,$rps)= eat_addr_port('Wait|Command'); $ra= conv_host_addr($ras); $rp= conv_port_number($rps); $rs= pack_sockaddr_in $rp,$ra; $_= shift @ARGV; m/^([.0-9]+),([.0-9]+),(\d+),(slip|cslip)$/ or quit("lvaddr,rvaddr,mtu,proto missing or bad syntax or proto not [c]slip"); ($lva,$rva,$mtu,$proto) = ($1,$2,$3,$4); $_= shift @ARGV; if (m/^(\d+),(\d+)$/) { ($keepalive,$timeout,$reannounce)= ($1+0,$2+0,0); $ka_to_ra= "$keepalive,$timeout"; } elsif (m/^(\d+),(\d+),(\d+)$/) { ($keepalive,$timeout,$reannounce)= ($1+0,$2+0,$3); "$keepalive,$timeout", $ka_to_ra= "$keepalive,$timeout,$reannounce"; } else { quit("keepalive,timeout missing or bad syntax"); } $keepalive && ($timeout > $keepalive*2) or quit("timeout must be < 2*keepalive") if $timeout; # Variables \$[lr]exn # Local/Remote Extra Nets $lexn= shift @ARGV; $rexn= shift @ARGV; defined($udp= getprotobyname('udp')) or fail("getprotobyname udp"); socket(L,PF_INET,SOCK_DGRAM,$udp) or fail("socket"); bind(L,$ls) or quit("bind failed: $!"); defined($ls= getsockname(L)) or fail("getsockname"); $lad= show_addr($ls); $lpd= show_port($ls); $lapd= "$lad,$lpd"; print "$lapd\n" or fail("print addr/port") if ($las eq 'Print' || $lps eq 'Print'); $rapcmd= ($ras eq 'Command' || $rps eq 'Command'); quit("need remote-command if Command for remote addr/port") if $rapcmd && !@ARGV; sub xform_remote ($$) { my ($showed,$spec) = @_; return 'Print' if $spec eq 'Command'; return 'Any' if $spec eq 'Wait'; return $showed; } if (@ARGV) { warning("-d specified with remote command, ignoring") if $dump; $dump= 1; $rad= xform_remote(show_addr($rs),$ras); $rpd= xform_remote(show_port($rs),$rps); @rcmd= (@ARGV, @remoteopts, "$rad,$rpd", $masq ? 'Wait,Wait' : $las eq 'Any' ? "Wait,$lpd" : $lapd, "$rva,$lva,$mtu,$proto", $ka_to_ra, $rexn, $lexn); debug("remote command @rcmd"); if ($rapcmd) { pipe(RAPREAD,RCMDREADSUB) or fail("pipe"); } pipe(RCMDWRITESUB,DUMPKEYS) or fail("pipe"); defined($c_rcmd= fork) or fail("fork for remote"); if (!$c_rcmd) { open STDIN, "<&RCMDWRITESUB" or fail("reopen stdin for remote command"); open STDOUT, ">&RCMDREADSUB" or fail("reopen stdout for remote command") if $rapcmd; close RAPREAD if $rapcmd; close DUMPKEYS; close RCMDWRITESUB; close RCMDREADSUB; close L; exec @rcmd; fail("failed to execute remote command $rcmd[0]"); } close RCMDWRITESUB; if ($rapcmd) { close RCMDREADSUB if $rapcmd; $_= ''; while (!m/\n/) { $!=0; defined($nread= sysread(RAPREAD,$_,1,length)) or fail("read from remote command"); if (!$nread) { close DUMPKEYS; close RAPREAD; waitpid $c_rcmd,0 or fail("wait for remote command"); quit($? ? "remote command failed (code $?)" : "no details received from remote"); } } chomp; m/^([.0-9]+)\,(\d+)$/ or quit("invalid details from remote end: \`$_'"); ($rar,$rpr) = ($1,$2); $ra= conv_host_addr($rar); $rp= conv_port_number($rpr); defined($c_catremdebug= fork) or fail("fork for cat remote debug"); if (!$c_catremdebug) { open(STDIN,"<&RAPREAD") or fail("redirect remote debug"); close DUMPKEYS; close L; exec "cat"; fail("execute cat"); } close RAPREAD; } } elsif ($dump) { open DUMPKEYS, ">&STDOUT" or fail("reopen stdout for key material"); $dump= 1; } else { open DUMPKEYS, "<&STDIN" or fail("reopen stdout for key material"); } $rs= pack_sockaddr_in $rp,$ra; if ($ras eq 'Wait' || $rps eq 'Wait') { @rapf= (''); $rapd= ('Wait,Wait'); } else { @rapf= (show_addr($rs), show_port($rs)); $rapd= show_addr_port($rs); } @lcmd= qw(userv root ipif) unless @lcmd; debug("using remote $rapd local $lapd"); push @lcmd, ("$lva,$rva,$mtu,$proto",$lexn); debug("local command @lcmd."); pipe(UR,UW) or fail("up pipe"); pipe(DR,DW) or fail("down pipe"); defined($c_lcmd= fork) or fail("fork for local command"); if (!$c_lcmd) { close UR; close DW; open(STDIN,"<&DR") or fail("reopen stdin for packets"); open(STDOUT,">&UW") or fail("reopen stdout for packets"); exec @lcmd; quit("cannot execute $lcmd[0]: $!"); } close UW; close DR; $xfwdopts.= 'w' if $dump; @fcmd= ($fcmd, $xfwdopts, fileno(L), fileno(DW), fileno(UR), fileno(DUMPKEYS), $mtu, $keepalive, $timeout, $reannounce, @rapf, @encryption); debug("forwarding command @fcmd."); defined($c_fwd= fork) or fail("fork for udptunnel-forwarder"); if (!$c_fwd) { foreach $fd (qw(L DW UR DUMPKEYS)) { fcntl($fd, F_SETFD, 0) or fail("set no-close-on-exec $fd"); } exec @fcmd; fail("cannot execute $fcmd[0]"); } close L; close DUMPKEYS; close UR; close DW; %procs= ($c_fwd, 'forwarder', $c_lcmd, 'local command'); $procs{$c_rcmd}= 'remote command' if $c_rcmd; $procs{$c_catremdebug}= 'debug cat' if $c_catremdebug; $estatus= 0; while (keys %procs) { ($c= wait) >0 or fail("wait failed (expecting ". join('; ',keys %procs). ")"); $status= $?; warning("unexpected child reaped: pid $c, code $status"), next unless exists $procs{$c}; $str= $procs{$c}; delete $procs{$c}; $status ? warning("subprocess $str failed with code $status") : debug("subprocess $str finished"); if ($c==$c_lcmd || $c==$c_fwd || $c==$c_rcmd) { kill 15, grep (exists $procs{$_}, $c_fwd, $c_rcmd); } $estatus=1 unless $c == $c_catremdebug; } debug("all processes terminated, exiting with status $estatus"); exit $estatus;