chiark / gitweb /
Works at least without crypto.
authorian <ian>
Tue, 30 May 2000 22:49:36 +0000 (22:49 +0000)
committerian <ian>
Tue, 30 May 2000 22:49:36 +0000 (22:49 +0000)
ipif/forwarder.c
ipif/udptunnel

index 9b7e51ee554181a270b77254d8428ce342bd1ef3..a9e43e096e16bd8c62b38a92c7241fa70b8dcddd 100644 (file)
@@ -107,9 +107,12 @@ static void inbound(void) {
   int r, i, different, this_saddrlen;
   const char *emsg;
 
+  buf_in.start= buf_in.base+1;
+  buf_in.size= buffer_size-2;
+  
   setnonblock(public_local_fd,1);
   this_saddrlen= sizeof(this_saddr);
-  r= recvfrom(public_local_fd, buf_in.base, buffer_size-1, 0,
+  r= recvfrom(public_local_fd, buf_in.start, buf_in.size, 0,
              &this_saddr, &this_saddrlen);
   if (!r) { diag("empty ciphertext"); return; }
 
@@ -118,14 +121,14 @@ static void inbound(void) {
     return;
   }
   if (this_saddr.sin_family != AF_INET) {
-    fprintf(stderr,"%s: received unknown AF %lu",
+    fprintf(stderr,"%s: received unknown AF %lu\n",
            programid, (unsigned long)this_saddr.sin_family);
     return;
   }
   assert(this_saddrlen == sizeof(this_saddr));
 
-  buf_in.size= buffer_size;
-  buf_in.start= buf_in.base;
+  assert(r <= buf_in.size);
+  buf_in.size= r;
   for (i=n_mechs-1; i>=0; i--) {
     emsg= mechs[i]->decode(md_in[i],&buf_in);
     if (emsg) {
@@ -136,21 +139,22 @@ static void inbound(void) {
 
   alarm(timeout);
 
-  different= !public_remote_specd ||
-    memcmp(&this_saddr,&public_remote,sizeof(this_saddr));
+  different= (!public_remote_specd ||
+             public_remote.sin_addr.s_addr != this_saddr.sin_addr.s_addr ||
+             public_remote.sin_port != this_saddr.sin_port);
 
   if (different) {
 
     if (public_remote_specd==2) {
-      fprintf(stderr, "%s: packet from unexpected sender %s:%lu",
+      fprintf(stderr, "%s: packet from unexpected sender %s:%lu\n",
              programid, inet_ntoa(this_saddr.sin_addr),
-             (unsigned long)this_saddr.sin_port);
+             (unsigned long)ntohs(this_saddr.sin_port));
       return;
     }
 
-    fprintf(stderr, "%s: tunnel open with peer %s:%lu",
+    fprintf(stderr, "%s: tunnel open with peer %s:%lu\n",
            programid, inet_ntoa(this_saddr.sin_addr),
-           (unsigned long)this_saddr.sin_port);
+           (unsigned long)ntohs(this_saddr.sin_port));
     nextsendka= now();
     public_remote_specd= 1;
     memcpy(&public_remote,&this_saddr,sizeof(public_remote));
@@ -162,10 +166,14 @@ static void inbound(void) {
   }
 
   any_recvd= 1;
-  
-  buf_in.start[buf_in.size]= 0300;
-  *--buf_in.start= 0300;
-  buf_in.size+= 2;
+
+  if (!buf_in.size || *buf_in.start != 0300) {
+    *--buf_in.start= 0300;
+    buf_in.size++;
+  }
+  if (buf_in.start[buf_in.size-1] != 0300) {
+    buf_in.start[buf_in.size++]= 0300;
+  }
 
   setnonblock(private_in_fd,0);
   write_must(private_in_fd, buf_in.start, buf_in.size, "write down");
@@ -213,7 +221,7 @@ static void outbound(void) {
     after_eaten= accum_buf;
     while ((delim= memchr(after_eaten, 0300, accum_used))) {
       this_packet= delim - after_eaten;
-      sendpacket(after_eaten, this_packet);
+      if (this_packet) sendpacket(after_eaten, this_packet);
       accum_used -= this_packet+1;
       after_eaten = delim+1;
     }
@@ -248,8 +256,9 @@ int main(int argc, const char *const *const argv_in) {
   arg= getarg_string();
   if (*arg) {
     public_remote_specd= 1;
+    public_remote.sin_family= AF_INET;
     arg_assert(inet_aton(arg,&public_remote.sin_addr));
-    public_remote.sin_port= getarg_ulong();
+    public_remote.sin_port= htons(getarg_ulong());
   }
 
   encdec_keys_fd= getarg_ulong();
@@ -280,7 +289,7 @@ int main(int argc, const char *const *const argv_in) {
 
     if (keepalive) {
       tnow= now();
-      if (tnow >= nextsendka) sendpacket("\300",1);
+      if (tnow >= nextsendka && public_remote_specd) sendpacket("\300",1);
       polltimeout= (nextsendka - tnow)*1000;
     } else {
       polltimeout= -1;
@@ -291,7 +300,7 @@ int main(int argc, const char *const *const argv_in) {
     if (r==-1 && errno==EINTR) continue;
     if (r==-1) sysfail("poll");
 
-    if (pollfds[0].revents & POLLIN) inbound();
-    if (pollfds[1].revents & POLLOUT) outbound();
+    if (pollfds[0].revents & (POLLIN|POLLERR)) inbound();
+    if (pollfds[1].revents & (POLLIN|POLLERR)) outbound();
   }
 }
index d7a00368cc96308c4dfe14a2e2f6496ac957beef..c2da428f9a2a5d020cc4875fa66383650b574103 100755 (executable)
@@ -4,7 +4,11 @@
 # usage:
 #  udptunnel
 #        [ -l[<local-command/arg>] ... .
-#          -e<encryption-mech>[/<encryption-parameter>...]
+#        | -e <encryption-mech>[/<encryption-parameter>...]
+#        | -m   (`masquerade support': subcommand gets `Wait' instead of our addr/port)
+#        | -d   (`dump keys': when no subcmd, spew keys rather than reading them;
+#                we always send keys to our subcmd if there is one)
+#        | -f<path-to-udptunnel-forwarder>
 #          ...
 #        ]
 #            <public-local-addr>,<public-local-port>
 #            <extra-local-nets> <extra-remote-nets>
 #          [ <remote-command> [<remote-args> ...] ]
 #
+# proto may be slip or cslip
 #
-# <..-addr> may also be hostname
+# Any <..-addr> may also be hostname
 #
-# <local-public-port> may be number or `print' or `silent'
+# Local addr's and ports may also be:
+#    `Print'     choose one ourselves and print both port and addr
+#    `Any'       choose one ourselves and do not print it
+# Remote addr's and ports may also be:
+#    `Wait'      wait to receive a packet before assigning address
+#    `Command'   run a subcommand and wait for it to tell us the values
+# When any addr or port is `Command' then <remote-command> must be specified.
 #
-# <remote-public-port> may number or `command', in which case
-# <remote-command> must be specified and should run udptunnel at the
+# If <remote-command> is specified it should run udptunnel at the
 # remote end; it will be invoked as
-#    <remote-command> <public-remote-addr>,print
-#                     <public-local-addr>,<public-local-port>
+#    <remote-command> [ <-e arguments passed along> ]
+#                     <public-remote-addr'>,<public-remote-port'>
+#                     <public-local-addr'>,<public-local-port'>
 #                     <private-remote-addr>,<private-local-addr>,<mtu>,<proto>
 #                     <keepalive>,<timeout>
 #                     <extra-remote-nets> <extra-local-nets>
 #
+
+# If it was given Print for <public-remote-foo'>, this command's first
+# stdout output should be the real
+# <public-remote-addr>,<public-remote-port> pair (and of course this
+# udptunnel's output will be).  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
+# parameters will be fed into its stdin.  See the documentation in the
+# mech-*.c files for details of the parameters.  udptunnel will
+# arrange to feed the keys fd of udptunnel-forwarder into the stdin of
+# the remote command.
+#
+# <public-remote-foo'> is as follows:
+#   <public-remote-foo>  <public-remote-foo'>
+#   actual addr/port     that addr/port
+#   `Command'            `Print'
+#   `Wait'               `Any'
+#
+# <public-local-foo'> is as follows:
+#   <public-local-foo>   <public-local-foo'>     <public-local-foo'>
+#                        (-m not specified)      (-m specified)
+#   actual addr/port     that addr/port         `Wait'
+#   `Wait'               the chosen address     `Wait'
+#   `Print'              the chosen address     `Wait'
+#
 # udptunnel will userv ipif locally, as
 #    userv root ipif <private-local-addr>,<private-remote-addr>,<mtu>,<proto>
 #                    <extra-local-nets>
-# or, if -l was given, userv root ipif is replaced with the argument(s) to
-# successive -l options.
+# 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.
 
-# Copyright (C) 1999 Ian Jackson
+# 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
@@ -64,58 +111,105 @@ $? 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]: $!\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/^([^,]+)\,(\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]);
+}
+sub show_port ($) {
     my ($s,@s) = @_;
     @s= unpack_sockaddr_in($s);
-    return inet_ntoa($s[1]).','.$s[0];
+    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;
+}
+
+$|=1;
 
 @lcmd= ();
+@encryption= ();
+$masq= 0;
+$dump= 0;
+$fcmd= 'udptunnel-forwarder';
 
 while ($ARGV[0] =~ m/^-/) {
     $_= shift @ARGV;
-    last if $_ eq '--';
-    if (s/^-l//) {
-       push @lcmd,$_ if length;
-       while (@ARGV && ($_= shift @ARGV) ne '.') { push @lcmd, $_; }
-    } else {
-       quit("unknown option \`$_'");
+    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 @encrargs, "-e$encrarg";
+           push @encryption, split m#/#, $encrarg;
+       } elsif (s/^-m/-/) {
+           $masq= 1;
+       } elsif (s/^-d/-/) {
+           $dump= 1;
+       } 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)$/
@@ -128,63 +222,120 @@ m/^(\d+),(\d+)$/ or 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";
+
+print "$lapd\n" or fail("print addr/port") if ($las eq 'Print' || $lps eq 'Print');
 
-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);
+$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,
+           @encrargs,
+           "$rad,$rpd",
+           $masq ? 'Wait,Wait' : $lapd,
+           "$rva,$lva,$mtu,$proto",
+           "$keepalive,$timeout",
+           $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");
+       select(RCMDREADSUB); $|=1; select(STDOUT);
     }
-    $_= <C>;
-    if (!length) {
-       close C;
-       quit($? ? "remote command failed (code $?)" : "no details received from remote");
+    pipe(DUMPKEYS,RCMDWRITESUB) 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;
+       $!=0; $_= <RAPREAD>; $e="$!";
+
+       defined($c_catremdebug= fork) or fail("fork for cat remote debug");
+       if (!$c_catremdebug) {
+           open(STDIN,"<&RAPREAD") or fail("redirect remote debug");
+           close RAPREAD;
+           close DUMPKEYS;
+           close L;
+           exec "cat"; fail("execute cat");
+       }
+       close RAPREAD;
+       
+       if (!length) {
+           close DUMPKEYS;
+           waitpid $c_rcmd,0 or fail("wait for remote command");
+           quit($? ? "remote command failed (code $?)" :
+                $e ? "read error from remote command: $e" :
+                "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);
     }
+} 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");
-push @lcmd, ("$lva,$rva,$mtu,$proto",$lepn);
-debug("local command @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($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");
@@ -194,82 +345,49 @@ if (!$c3) {
 close UW;
 close DR;
 
-$upyet= 0;
-$downyet= 0;
-
-$wantreadfds='';
-vec($wantreadfds,fileno(UR),1)= 1;
-vec($wantreadfds,fileno(L),1)= 1;
+@fcmd= ($fcmd,
+       fileno(L), fileno(DW), fileno(UR),
+       $mtu, $keepalive, $timeout,
+       @rapf,
+       fileno(DUMPKEYS), $dump ? 'y' : '',
+       @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)) {
+       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();
-       $thistimeout= $nextsendka-$now;
-       if ($thistimeout < 0) {
-           defined(send L,"\300",0,$rs)
-               or warning("transmit keepalive error: $!");
-           $nextsendka= $now+$keepalive;
-           $thistimeout= $keepalive;
-       }
-    } else {
-       $thistimeout= undef;
-    }
-    select($readfds=$wantreadfds,'','',$thistimeout);
-    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 ($p && !defined(send L,substr($upbuf,0,$p),0,$rs)) {
-               warning("transmit error: $!");
-           } else {
-               if (!$upyet) {
-                   $upyet= 1;
-                   debug($downyet ? "tunnel open at this end" : "transmitting");
-               }
-               if ($keepalive) { $nextsendka= now()+$keepalive; }
-           }
-           $upbuf= substr($upbuf,$p+1);
-       }
+while (keys %procs) {
+    ($c= wait) >0 or
+       fail("wait failed (expecting ". join('; ',keys %procs). ")");
+    warning("unexpected child reaped: pid $c, code $?"), next
+       unless exists $procs{$c};
+    $str= $procs{$c};
+    delete $procs{$c};
+    $? ? warning("subprocess $str failed with code $?")
+       : debug("subprocess $str finished");
+    if ($c==$c_lcmd || $c==$c_fwd || $c==$c_rcmd) {
+       kill 15, grep (exists $procs{$_}, $c_fwd, $c_rcmd);
     }
-    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;
-       }
-       $downbuf= "\300".$downbuf."\300";
-       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" : "receiving");
-           }
-           alarm($timeout) if $timeout;
-       }
-    }
-    if ($! == ECONNREFUSED) { quit("tunnel closed at remote end"); }
-    $! == EAGAIN || warning("receive error: $!");
+    $estatus=1 unless $c == $c_catremdebug;
 }
+
+debug("all processes terminated, exiting with status $estatus");
+
+exit $estatus;