#!/usr/bin/perl
-# Simple tunnel for userv-ipif tunnels.
+# Encrypting VPN tunnel for use with userv-ipif.
+#
+# This comment is reference documentation. See ipif/INSTALL for the
+# installation instructions and setup tutorial.
#
# usage:
-# udptunnel
-# <public-local-host/addr>,<public-local-port>
-# <public-remote-host/addr>,<public-remote-port>
-# <private-local-addr>,<private-remote-addr>,<mtu>,<proto>
-# <keepalive>,<timeout>
-# <extra-local-nets> <extra-remote-nets>
-# [ <remote-command> [<remote-args> ...] ]
-#
-# <local-public-port> may be number or `print' or `silent'
-#
-# <remote-public-port> may number or `command', in which case
-# <remote-command> must be specified and should run udptunnel at the
-# remote end; it will be invoked as
-# <remote-command> <public-remote-host/addr>,print
-# <public-local-addr>,<public-local-port>
-# <private-remote-addr>,<private-local-addr>,<mtu>,<proto>
-# <keepalive>,<timeout>
-# <extra-remote-nets> <extra-local-nets>
-#
-# udptunnel will userv ipif locally, as
-# userv root ipif <private-local-addr>,<private-remote-addr>,<mtu>,<proto>
-# <extra-local-nets>
+# To make a tunnel between two machines `alice' and `bob',
+# on `alice', the active endpoint, run:
+#
+# udptunnel
+# [ -l[<alice-command/arg>] ... .
+# | -e <encryption-mech>[/<encryption-parameter>...]
+# | -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<path-to-udptunnel-forwarder>
+# ...
+# ]
+# <alice-phys-addr>,<alice-phys-port>
+# <bob-phys-addr>,<bob-phys-port>
+# <alice-virt-addr>,<bob-virt-addr>,<mtu>,<proto>
+# <keepalive>,<timeout>[,<reannounce>]
+# <alice-priv-nets> <bob-priv-nets>
+# [ <bob-command> [<bob-args> ...] ]
+#
+# This will run udptunnel-forwarder on alice, and use <bob-command>
+# (usually an ssh invocation) to run udptunnel appropriately on bob.
+# Key material will be generated by alice and fed to udptunnel on bob
+# via <bob-command>'s stdin, and the physical address and port on bob
+# will be (if so configured) returned via <bob-command>'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.
+#
+# <proto> may be slip or cslip
+#
+# <mtu> 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
+# <bob-command>, 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 <bob-command> to tell us the values (this is
+# usually the right choice on alice for at least the
+# port). <bob-command> 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 <bob-command> is specified it should run udptunnel at the
+# bob end; it will be invoked as
+# <bob-command> [ <bob-args> ... ]
+# [ <-e arguments passed along> ]
+# <bob-phys-addr'>,<bob-phys-port'>
+# <alice-phys-addr'>,<alice-phys-port'>
+# <bob-virt-addr>,<alice-virt-addr>,<mtu>,<proto>
+# <keepalive>,<timeout>[,<reannounce>]
+# <bob-priv-nets> <alice-priv-nets>
+#
+# If it was given Print for <bob-phys-foo'>, udptunnel's first stdout
+# output will be the real <bob-phys-addr>,<bob-phys-port> 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.
+#
+# <bob-phys-foo'> is as follows:
+# <bob-phys-foo> <bob-phys-foo'>
+# actual addr/port that addr/port
+# `Command' `Print'
+# `Wait' `Any'
+#
+# <alice-phys-foo'> is as follows:
+# <alice-phys-foo> <alice-phys-foo'> <alice-phys-foo'>
+# (-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 <local-virt-addr>,<remote-virt-addr>,<mtu>,<proto>
+# <remote-priv-nets>
+# 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/<max-skew>/<max-age> (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 <max-skew> is perhaps 10 and <max-age> 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 (C) 1999-2000 Ian Jackson
+#
+# 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 2 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, write to the Free Software
+# Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# $Id$
use Socket;
use POSIX;
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]: $!\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("<host/addr>,<port> missing");
+ @ARGV or quit("<addr>,<port> missing");
$_= shift(@ARGV);
- $_ =~ m/^([^,]+)\,(\d+|$x)$/ or quit("$_: <host/addr>,<port> bad syntax");
+ (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("$_: <host/addr>,<port> bad syntax".
+ (m/[A-Z]/ ? ' (use lowercase for hostnames)' : ''));
return ($1,$2);
}
sub conv_host_addr ($) {
- my ($s,$r) = @_;
- defined($r= inet_aton($s)) or quit("$s: cannot convert to address");
+ 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) = @_;
- if ($s =~ m/\d/) {
- $r= $s+0;
- $r>0 && $r<65536 or quit("$s: port out of range");
- } else {
- $r= 0;
- }
+ 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_port ($) {
+sub show_addr ($) {
my ($s,@s) = @_;
@s= unpack_sockaddr_in($s);
- return inet_ntoa($s[1]).','.$s[0];
+ 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 \`$_'");
+ }
+ }
}
-($las,$lps)= eat_addr_port('print|silent');
+# 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('command');
+($ras,$rps)= eat_addr_port('Wait|Command');
+$ra= conv_host_addr($ras);
$rp= conv_port_number($rps);
-$ra= $rps eq 'command' ? '' : conv_host_addr($ras);
+$rs= pack_sockaddr_in $rp,$ra;
$_= shift @ARGV;
m/^([.0-9]+),([.0-9]+),(\d+),(slip|cslip)$/
($lva,$rva,$mtu,$proto) = ($1,$2,$3,$4);
$_= shift @ARGV;
-m/^(\d+),(\d+)$/ or quit("keepalive,timeout missing or bad syntax");
-($keepalive,$timeout)= ($1,$2);
+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;
-$lepn= shift @ARGV;
-$repn= shift @ARGV;
-
-alarm($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");
-$lsp= show_addr_port($ls);
+$lad= show_addr($ls);
+$lpd= show_port($ls);
+$lapd= "$lad,$lpd";
-if ($rps eq 'command') {
- quit("when using ,command for remote, must supply command") unless @ARGV;
- @rcmd= (@ARGV, "$ras,print", "$lsp", "$rva,$lva,$mtu,$proto",
- "$keepalive,$timeout", $repn, $lepn);
+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");
- defined($c= open C,"-|") or fail("fork for remote");
- if (!$c) {
- exec @rcmd; die "$progname: error: failed to execute $rcmd[0]: $!\n";
+
+ if ($rapcmd) {
+ pipe(RAPREAD,RCMDREADSUB) or fail("pipe");
}
- $_= <C>;
- if (!length) {
- close C;
- quit($? ? "remote command failed (code $?)" : "no details received from remote");
+ 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]");
}
- chomp;
- m/^([.0-9]+)\,(\d+)$/ or quit("invalid details from remote end ($_)");
- ($ras,$rps) = ($1,$2);
- $ra= conv_host_addr($ras);
- $rp= conv_port_number($rps);
- defined($c2= fork) or fail("fork for cat");
- if (!$c2) {
- open(STDIN,"<&C") or fail("redirect remote pipe to stdin");
- close C;
- exec "cat"; fail("execute cat");
+ 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 {
- quit("when not using ,command for remote, must not supply command") if @ARGV;
+ open DUMPKEYS, "<&STDIN" or fail("reopen stdout for key material");
}
$rs= pack_sockaddr_in $rp,$ra;
-$rsp= show_addr_port($rs);
-if ($lps eq 'print') { print($lsp,"\n") or quit("write port to stdout: $!"); }
+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 $rsp local $lsp");
+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($c3= fork) or fail("fork for ipif");
-if (!$c3) {
+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 "userv","root","ipif","$lva,$rva,$mtu,$proto",$lepn;
- quit("cannot execute userv ipif: $!");
+ exec @lcmd;
+ quit("cannot execute $lcmd[0]: $!");
}
close UW;
close DR;
-$upyet= 0;
-$downyet= 0;
+$xfwdopts.= 'w' if $dump;
-$wantreadfds='';
-vec($wantreadfds,fileno(UR),1)= 1;
-vec($wantreadfds,fileno(L),1)= 1;
+@fcmd= ($fcmd, $xfwdopts,
+ fileno(L), fileno(DW), fileno(UR), fileno(DUMPKEYS),
+ $mtu, $keepalive, $timeout, $reannounce,
+ @rapf,
+ @encryption);
+debug("forwarding command @fcmd.");
-sub nonblock ($) {
- my ($fh,$fl) = @_;
- ($fl= fcntl($fh,F_GETFL,0)) or fail("nonblock F_GETFL");
- $fl |= O_NONBLOCK;
- fcntl($fh, F_SETFL, $fl) or fail("nonblock F_SETFL");
+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]");
}
-nonblock('UR');
-nonblock('L');
+close L;
+close DUMPKEYS;
+close UR;
+close DW;
-$upbuf= '';
+%procs= ($c_fwd, 'forwarder',
+ $c_lcmd, 'local command');
+$procs{$c_rcmd}= 'remote command' if $c_rcmd;
+$procs{$c_catremdebug}= 'debug cat' if $c_catremdebug;
-sub now () { my ($v); defined($v= time) or fail("get time"); return $v; }
-if ($keepalive) { $nextsendka= now(); }
+$estatus= 0;
-for (;;) {
- if ($keepalive) {
- $now= now();
- $timeout= $nextsendka-$now;
- if ($timeout < 0) {
- defined(send L,"\300",0,$rs)
- or warning("transmit keepalive error: $!");
- $nextsendka= $now+$keepalive;
- $timeout= $keepalive;
- }
- } else {
- $timeout= undef;
+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);
}
- select($readfds=$wantreadfds,'','',$timeout);
- for (;;) {
- if (!defined($r= sysread(UR,$upbuf,$mtu*2+3,length($upbuf)))) {
- $! == EAGAIN || warning("tunnel endpoint read error: $!");
- last;
- }
- if (!$r) {
- quit "tunnel endpoint closed by system";
- }
- while (($p= index($upbuf,"\300")) >= 0) {
- if (!defined(send L,substr($upbuf,0,$p),0,$rs)) {
- warning("transmit error: $!");
- } elsif (!$upyet) {
- $upyet= 1;
- debug($downyet ? "tunnel open at this end" : "transmit channel open");
- }
- $upbuf= substr($upbuf+1,$p);
- }
- }
- while (defined($rs_from= recv L,$downbuf,$mtu*2+3,0)) {
- $rsp_from= show_addr_port($rs_from);
- if ($rsp_from ne $rsp) {
- warning("got packet from incorrect peer $rsp_from");
- next;
- }
- if (!defined($r= syswrite(DW,$downbuf,length $downbuf))) {
- warning("tunnel endpoint write error: $!");
- } elsif ($r != length $downbuf) {
- warning("tunnel endpoint wrong write length");
- } else {
- if (!$downyet) {
- $downyet= 1;
- debug($upyet ? "tunnel open at this end" : "receive channel open");
- }
- alarm($timeout) if $timeout;
- }
- }
- $! == EAGAIN || warning("receive error: $!");
+ $estatus=1 unless $c == $c_catremdebug;
}
+
+debug("all processes terminated, exiting with status $estatus");
+
+exit $estatus;