#!/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[<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 1996-2013 Ian Jackson <ijackson@chiark.greenend.org.uk>
# Copyright 1998 David Damerell <damerell@chiark.greenend.org.uk>
# Copyright 1999,2003
#    Chancellor Masters and Scholars of the University of Cambridge
# Copyright 2010 Tony Finch <fanf@dotat.at>
#
# 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("<addr>,<port> 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("$_: <host/addr>,<port> 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;
