Works at least without crypto.
[userv-utils] / ipif / udptunnel
1 #!/usr/bin/perl
2 # Simple tunnel for userv-ipif tunnels.
3 #
4 # usage:
5 # udptunnel
6 # [ -l[<local-command/arg>] ... .
7 # | -e <encryption-mech>[/<encryption-parameter>...]
8 # | -m (`masquerade support': subcommand gets `Wait' instead of our addr/port)
9 # | -d (`dump keys': when no subcmd, spew keys rather than reading them;
10 # we always send keys to our subcmd if there is one)
11 # | -f<path-to-udptunnel-forwarder>
12 # ...
13 # ]
14 # <public-local-addr>,<public-local-port>
15 # <public-remote-addr>,<public-remote-port>
16 # <private-local-addr>,<private-remote-addr>,<mtu>,<proto>
17 # <keepalive>,<timeout>
18 # <extra-local-nets> <extra-remote-nets>
19 # [ <remote-command> [<remote-args> ...] ]
20 #
21 # proto may be slip or cslip
22 #
23 # Any <..-addr> may also be hostname
24 #
25 # Local addr's and ports may also be:
26 # `Print' choose one ourselves and print both port and addr
27 # `Any' choose one ourselves and do not print it
28 # Remote addr's and ports may also be:
29 # `Wait' wait to receive a packet before assigning address
30 # `Command' run a subcommand and wait for it to tell us the values
31 # When any addr or port is `Command' then <remote-command> must be specified.
32 #
33 # If <remote-command> is specified it should run udptunnel at the
34 # remote end; it will be invoked as
35 # <remote-command> [ <-e arguments passed along> ]
36 # <public-remote-addr'>,<public-remote-port'>
37 # <public-local-addr'>,<public-local-port'>
38 # <private-remote-addr>,<private-local-addr>,<mtu>,<proto>
39 # <keepalive>,<timeout>
40 # <extra-remote-nets> <extra-local-nets>
41 #
42
43 # If it was given Print for <public-remote-foo'>, this command's first
44 # stdout output should be the real
45 # <public-remote-addr>,<public-remote-port> pair (and of course this
46 # udptunnel's output will be). It may then produce more stdout which,
47 # if any, will be forwarded to the local end's stdout as debugging info.
48 #
49 # After this, if any encryption was specified, the encryption
50 # parameters will be fed into its stdin. See the documentation in the
51 # mech-*.c files for details of the parameters. udptunnel will
52 # arrange to feed the keys fd of udptunnel-forwarder into the stdin of
53 # the remote command.
54 #
55 # <public-remote-foo'> is as follows:
56 # <public-remote-foo> <public-remote-foo'>
57 # actual addr/port that addr/port
58 # `Command' `Print'
59 # `Wait' `Any'
60 #
61 # <public-local-foo'> is as follows:
62 # <public-local-foo> <public-local-foo'> <public-local-foo'>
63 # (-m not specified) (-m specified)
64 # actual addr/port that addr/port `Wait'
65 # `Wait' the chosen address `Wait'
66 # `Print' the chosen address `Wait'
67 #
68 # udptunnel will userv ipif locally, as
69 # userv root ipif <private-local-addr>,<private-remote-addr>,<mtu>,<proto>
70 # <extra-local-nets>
71 # or, if -l was given, userv root ipif is replaced with the argument(s)
72 # following -l option(s) until `.'.
73 #
74 # udptunnel will also run udptunnel-forwarder with appropriate options
75 #
76 # recommended encryption parameters are:
77 # -e nonce (prepend 32 bit counter)
78 # -e timestamp/<max-skew>/<max-age> (prepend 32 bit time_t, and check on receipt)
79 # -e pkcs5/8 (pad as per PKCS#5 to 8-byte boundary)
80 # -e blowfish-cbcmac/128 (prepend CBC MAC with random IV and 128 bit key)
81 # -e blowfish-cbc/128 (encrypt with CBC, random IV and 128 bit key)
82 # where <max-skew> is perhaps 10 and <max-age> perhaps 30.
83
84 # Copyright (C) 1999-2000 Ian Jackson
85 #
86 # This is free software; you can redistribute it and/or modify it
87 # under the terms of the GNU General Public License as published by
88 # the Free Software Foundation; either version 2 of the License, or
89 # (at your option) any later version.
90 #
91 # This program is distributed in the hope that it will be useful, but
92 # WITHOUT ANY WARRANTY; without even the implied warranty of
93 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
94 # General Public License for more details.
95 #
96 # You should have received a copy of the GNU General Public License
97 # along with userv-utils; if not, write to the Free Software
98 # Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
99 #
100 # $Id$
101
102 use Socket;
103 use POSIX;
104 use Fcntl;
105
106 $progname= $0; $progname =~ s,.*/,,;
107 $|=1;
108
109 chomp($hostname= `uname -n`);
110 $? and die "$progname: cannot get hostname (uname failed with code $?)\n";
111
112 sub quit ($) { die "$progname - $hostname: fatal error: $_[0]\n"; }
113 sub debug ($) { print "$progname - $hostname: debug: $_[0]\n"; }
114 sub fail ($) { quit("unexpected system call failure: $_[0]: $!"); }
115 sub warning ($) { warn "$progname - $hostname: $_[0]\n"; }
116
117 sub eat_addr_port ($) {
118 my ($x) = @_;
119 @ARGV or quit("<addr>,<port> missing");
120 $_= shift(@ARGV);
121 (m/^$x,/i && m/^[a-z]/ || m/,$x$/i && m/,[a-z]/)
122 and warning("$_: use Mixed Case for special values");
123 m/^([0-9a-z][0-9a-z-+.]+|$x)\,(\d+|$x)$/
124 or quit("$_: <host/addr>,<port> bad syntax".
125 (m/[A-Z]/ ? ' (use lowercase for hostnames)' : ''));
126 return ($1,$2);
127 }
128 sub conv_host_addr ($) {
129 my ($s,$r,@h) = @_;
130 return INADDR_ANY() if $s =~ m/^[A-Z][a-z]/;
131 return $r if defined($r= inet_aton($s));
132 @h= gethostbyname($s) or quit("$s: cannot get address");
133 $h[2] eq &AF_INET or quit("$s: address is not IPv4");
134 @h < 5 or quit("$s: name maps to no addresses");
135 $r= $h[4];
136 @h == 5 or warning("$s: name has several addresses, using ".inet_ntoa($r));
137 return $r;
138 }
139 sub conv_port_number ($) {
140 my ($s,$r) = @_;
141 return 0 if $s =~ m/^[A-Z][a-z]/;
142 $r= $s+0;
143 $r>0 && $r<65536 or quit("$s: port out of range");
144 return $r;
145 }
146 sub show_addr ($) {
147 my ($s,@s) = @_;
148 @s= unpack_sockaddr_in($s);
149 return inet_ntoa($s[1]);
150 }
151 sub show_port ($) {
152 my ($s,@s) = @_;
153 @s= unpack_sockaddr_in($s);
154 return $s[0];
155 }
156 sub show_addr_port ($) {
157 my ($s) = @_;
158 return show_addr($s).','.show_port($s);
159 }
160 sub arg_value ($$) {
161 my ($val,$opt);
162 $_= '-';
163 return $val if length $val;
164 @ARGV or quit("$opt needs value");
165 return shift @ARGV;
166 }
167
168 $|=1;
169
170 @lcmd= ();
171 @encryption= ();
172 $masq= 0;
173 $dump= 0;
174 $fcmd= 'udptunnel-forwarder';
175
176 while ($ARGV[0] =~ m/^-/) {
177 $_= shift @ARGV;
178 last if m/^--?$/;
179 while (!m/^-$/) {
180 if (s/^-l//) {
181 push @lcmd,$_ if length;
182 while (@ARGV && ($_= shift @ARGV) ne '.') { push @lcmd, $_; }
183 $_= '-'
184 } elsif (s/^-f//) {
185 $fcmd= arg_value($_,'-f');
186 } elsif (s/^-e//) {
187 $encrarg= arg_value($_,'-e');
188 push @encrargs, "-e$encrarg";
189 push @encryption, split m#/#, $encrarg;
190 } elsif (s/^-m/-/) {
191 $masq= 1;
192 } elsif (s/^-d/-/) {
193 $dump= 1;
194 } else {
195 quit("unknown option \`$_'");
196 }
197 }
198 }
199
200 # Variables \$[lr]a?p?(|s|d|r)
201 # Local/Remote Address&/Port
202 # actualvalue/Specified/Displaypassdown/fromRemote/passtoForwarder
203 #
204 ($las,$lps)= eat_addr_port('Print|Any');
205 $la= conv_host_addr($las);
206 $lp= conv_port_number($lps);
207 $ls= pack_sockaddr_in $lp,$la;
208
209 ($ras,$rps)= eat_addr_port('Wait|Command');
210 $ra= conv_host_addr($ras);
211 $rp= conv_port_number($rps);
212 $rs= pack_sockaddr_in $rp,$ra;
213
214 $_= shift @ARGV;
215 m/^([.0-9]+),([.0-9]+),(\d+),(slip|cslip)$/
216 or quit("lvaddr,rvaddr,mtu,proto missing or bad syntax or proto not [c]slip");
217 ($lva,$rva,$mtu,$proto) = ($1,$2,$3,$4);
218
219 $_= shift @ARGV;
220 m/^(\d+),(\d+)$/ or quit("keepalive,timeout missing or bad syntax");
221 ($keepalive,$timeout)= ($1,$2);
222 $keepalive && ($timeout > $keepalive*2) or quit("timeout must be < 2*keepalive")
223 if $timeout;
224
225 # Variables \$[lr]exn
226 # Local/Remote Extra Nets
227 $lexn= shift @ARGV;
228 $rexn= shift @ARGV;
229
230 defined($udp= getprotobyname('udp')) or fail("getprotobyname udp");
231
232 socket(L,PF_INET,SOCK_DGRAM,$udp) or fail("socket");
233 bind(L,$ls) or quit("bind failed: $!");
234 defined($ls= getsockname(L)) or fail("getsockname");
235 $lad= show_addr($ls);
236 $lpd= show_port($ls);
237 $lapd= "$lad,$lpd";
238
239 print "$lapd\n" or fail("print addr/port") if ($las eq 'Print' || $lps eq 'Print');
240
241 $rapcmd= ($ras eq 'Command' || $rps eq 'Command');
242 quit("need remote-command if Command for remote addr/port") if $rapcmd && !@ARGV;
243
244 sub xform_remote ($$) {
245 my ($showed,$spec) = @_;
246 return 'Print' if $spec eq 'Command';
247 return 'Any' if $spec eq 'Wait';
248 return $showed;
249 }
250
251 if (@ARGV) {
252 warning("-d specified with remote command, ignoring") if $dump;
253 $dump= 1;
254
255 $rad= xform_remote(show_addr($rs),$ras);
256 $rpd= xform_remote(show_port($rs),$rps);
257 @rcmd= (@ARGV,
258 @encrargs,
259 "$rad,$rpd",
260 $masq ? 'Wait,Wait' : $lapd,
261 "$rva,$lva,$mtu,$proto",
262 "$keepalive,$timeout",
263 $rexn, $lexn);
264 debug("remote command @rcmd");
265
266 if ($rapcmd) {
267 pipe(RAPREAD,RCMDREADSUB) or fail("pipe");
268 select(RCMDREADSUB); $|=1; select(STDOUT);
269 }
270 pipe(DUMPKEYS,RCMDWRITESUB) or fail("pipe");
271 defined($c_rcmd= fork) or fail("fork for remote");
272 if (!$c_rcmd) {
273 open STDIN, ">&RCMDWRITESUB" or fail("reopen stdin for remote command");
274 open STDOUT, ">&RCMDREADSUB" or fail("reopen stdout for remote command")
275 if $rapcmd;
276 close RAPREAD if $rapcmd;
277 close DUMPKEYS;
278 close RCMDWRITESUB;
279 close RCMDREADSUB;
280 close L;
281 exec @rcmd; fail("failed to execute remote command $rcmd[0]");
282 }
283 close RCMDWRITESUB;
284
285 if ($rapcmd) {
286 close RCMDREADSUB if $rapcmd;
287 $!=0; $_= <RAPREAD>; $e="$!";
288
289 defined($c_catremdebug= fork) or fail("fork for cat remote debug");
290 if (!$c_catremdebug) {
291 open(STDIN,"<&RAPREAD") or fail("redirect remote debug");
292 close RAPREAD;
293 close DUMPKEYS;
294 close L;
295 exec "cat"; fail("execute cat");
296 }
297 close RAPREAD;
298
299 if (!length) {
300 close DUMPKEYS;
301 waitpid $c_rcmd,0 or fail("wait for remote command");
302 quit($? ? "remote command failed (code $?)" :
303 $e ? "read error from remote command: $e" :
304 "no details received from remote");
305 }
306 chomp;
307 m/^([.0-9]+)\,(\d+)$/ or quit("invalid details from remote end: \`$_'");
308 ($rar,$rpr) = ($1,$2);
309 $ra= conv_host_addr($rar);
310 $rp= conv_port_number($rpr);
311 }
312 } elsif ($dump) {
313 open DUMPKEYS, ">&STDOUT" or fail("reopen stdout for key material");
314 $dump= 1;
315 } else {
316 open DUMPKEYS, "<&STDIN" or fail("reopen stdout for key material");
317 }
318
319 $rs= pack_sockaddr_in $rp,$ra;
320
321 if ($ras eq 'Wait' || $rps eq 'Wait') {
322 @rapf= ('');
323 $rapd= ('Wait,Wait');
324 } else {
325 @rapf= (show_addr($rs), show_port($rs));
326 $rapd= show_addr_port($rs);
327 }
328 @lcmd= qw(userv root ipif) unless @lcmd;
329
330 debug("using remote $rapd local $lapd");
331 push @lcmd, ("$lva,$rva,$mtu,$proto",$lexn);
332 debug("local command @lcmd.");
333
334 pipe(UR,UW) or fail("up pipe");
335 pipe(DR,DW) or fail("down pipe");
336
337 defined($c_lcmd= fork) or fail("fork for local command");
338 if (!$c_lcmd) {
339 close UR; close DW;
340 open(STDIN,"<&DR") or fail("reopen stdin for packets");
341 open(STDOUT,">&UW") or fail("reopen stdout for packets");
342 exec @lcmd;
343 quit("cannot execute $lcmd[0]: $!");
344 }
345 close UW;
346 close DR;
347
348 @fcmd= ($fcmd,
349 fileno(L), fileno(DW), fileno(UR),
350 $mtu, $keepalive, $timeout,
351 @rapf,
352 fileno(DUMPKEYS), $dump ? 'y' : '',
353 @encryption);
354 debug("forwarding command @fcmd.");
355
356 defined($c_fwd= fork) or fail("fork for udptunnel-forwarder");
357 if (!$c_fwd) {
358 foreach $fd (qw(L DW UR)) {
359 fcntl($fd, F_SETFD, 0) or fail("set no-close-on-exec $fd");
360 }
361 exec @fcmd; fail("cannot execute $fcmd[0]");
362 }
363
364 close L;
365 close DUMPKEYS;
366 close UR;
367 close DW;
368
369 %procs= ($c_fwd, 'forwarder',
370 $c_lcmd, 'local command');
371 $procs{$c_rcmd}= 'remote command' if $c_rcmd;
372 $procs{$c_catremdebug}= 'debug cat' if $c_catremdebug;
373
374 $estatus= 0;
375
376 while (keys %procs) {
377 ($c= wait) >0 or
378 fail("wait failed (expecting ". join('; ',keys %procs). ")");
379 warning("unexpected child reaped: pid $c, code $?"), next
380 unless exists $procs{$c};
381 $str= $procs{$c};
382 delete $procs{$c};
383 $? ? warning("subprocess $str failed with code $?")
384 : debug("subprocess $str finished");
385 if ($c==$c_lcmd || $c==$c_fwd || $c==$c_rcmd) {
386 kill 15, grep (exists $procs{$_}, $c_fwd, $c_rcmd);
387 }
388 $estatus=1 unless $c == $c_catremdebug;
389 }
390
391 debug("all processes terminated, exiting with status $estatus");
392
393 exit $estatus;