Home | History | Annotate | Download | only in misc
      1 #!/usr/bin/perl
      2 #
      3 # inet6to4:  Act as an ipv6-to-ipv4 relay for tcp applications that
      4 #            do not support ipv6.
      5 #
      6 # Usage:     inet6to4    <ipv6-listen-port> <ipv4-host:port>
      7 #            inet6to4 -r <ipv4-listen-port> <ipv6-host:port>
      8 #
      9 # Examples:  inet6to4 5900 localhost:5900
     10 #            inet6to4 8080 web1:80
     11 #            inet6to4 -r 5900 fe80::217:f2ff:fee6:6f5a%eth0:5900
     12 #
     13 # The -r option reverses the direction of translation (e.g. for ipv4
     14 # clients that need to connect to ipv6 servers.)  Reversing is the default
     15 # if this script is named 'inet4to6' (e.g. by a symlink.)
     16 #
     17 # Use Ctrl-C to stop this program.  You can also supply '-c n' as the
     18 # first option to only handle that many connections.
     19 #
     20 # Also set the env. vars INET6TO4_LOOP=1 or INET6TO4_LOOP=BG
     21 # to have an outer loop restarting this program (BG means do that
     22 # in the background), and INET6TO4_LOGFILE for a log file.
     23 # Also set INET6TO4_VERBOSE to verbosity level and INET6TO4_WAITTIME
     24 # and INET6TO4_PIDFILE (see below.)
     25 #
     26 
     27 #-------------------------------------------------------------------------
     28 # Copyright (c) 2010 by Karl J. Runge <runge (at] karlrunge.com>
     29 #
     30 # inet6to4 is free software; you can redistribute it and/or modify
     31 # it under the terms of the GNU General Public License as published by
     32 # the Free Software Foundation; either version 2 of the License, or (at
     33 # your option) any later version.
     34 # 
     35 # inet6to4 is distributed in the hope that it will be useful,
     36 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     37 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     38 # GNU General Public License for more details.
     39 # 
     40 # You should have received a copy of the GNU General Public License
     41 # along with inet6to4; if not, write to the Free Software
     42 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA
     43 # or see <http://www.gnu.org/licenses/>.
     44 #-------------------------------------------------------------------------
     45 
     46 my $program = "inet6to4";
     47 
     48 # Set up logging:
     49 #
     50 if (exists $ENV{INET6TO4_LOGFILE}) {
     51 	close STDOUT;
     52 	if (!open(STDOUT, ">>$ENV{INET6TO4_LOGFILE}")) {
     53 	        die "$program: $ENV{INET6TO4_LOGFILE} $!\n";
     54 	}
     55 	close STDERR;
     56 	open(STDERR, ">&STDOUT");
     57 }
     58 select(STDERR); $| = 1;
     59 select(STDOUT); $| = 1;
     60 
     61 # interrupt handler:
     62 #
     63 my $looppid = '';
     64 my $pidfile = '';
     65 my $listen_sock = '';	# declared here for get_out()
     66 #
     67 sub get_out {
     68 	print STDERR "$_[0]:\t$$ looppid=$looppid\n";
     69 	close $listen_sock if $listen_sock;
     70 	if ($looppid) {
     71 		kill 'TERM', $looppid;
     72 		fsleep(0.2);
     73 	}
     74 	unlink $pidfile if $pidfile;
     75 	exit 0;
     76 }
     77 $SIG{INT}  = \&get_out;
     78 $SIG{TERM} = \&get_out;
     79 
     80 # pidfile:
     81 #
     82 sub open_pidfile {
     83 	if (exists $ENV{INET6TO4_PIDFILE}) {
     84 		my $pf = $ENV{INET6TO4_PIDFILE};
     85 		if (open(PID, ">$pf")) {
     86 			print PID "$$\n";
     87 			close PID;
     88 			$pidfile = $pf;
     89 		} else {
     90 			print STDERR "could not open pidfile: $pf - $! - continuing...\n";
     91 		}
     92 		delete $ENV{INET6TO4_PIDFILE};
     93 	}
     94 }
     95 
     96 ####################################################################
     97 # Set INET6TO4_LOOP=1 to have this script create an outer loop
     98 # restarting itself if it ever exits.  Set INET6TO4_LOOP=BG to
     99 # do this in the background as a daemon.
    100 
    101 if (exists $ENV{INET6TO4_LOOP}) {
    102 	my $csl = $ENV{INET6TO4_LOOP};
    103 	if ($csl ne 'BG' && $csl ne '1') {
    104 		die "$program: invalid INET6TO4_LOOP.\n";
    105 	}
    106 	if ($csl eq 'BG') {
    107 		# go into bg as "daemon":
    108 		setpgrp(0, 0);
    109 		my $pid = fork();
    110 		if (! defined $pid) {
    111 			die "$program: $!\n";
    112 		} elsif ($pid) {
    113 			wait;
    114 			exit 0;
    115 		}
    116 		if (fork) {
    117 			exit 0;
    118 		}
    119 		setpgrp(0, 0);
    120 		close STDIN;
    121 		if (! $ENV{INET6TO4_LOGFILE}) {
    122 			close STDOUT;
    123 			close STDERR;
    124 		}
    125 	}
    126 	delete $ENV{INET6TO4_LOOP};
    127 
    128 	if (exists $ENV{INET6TO4_PIDFILE}) {
    129 		open_pidfile();
    130 	}
    131 
    132 	print STDERR "$program: starting service at ", scalar(localtime), " master-pid=$$\n";
    133 	while (1) {
    134 		$looppid = fork;
    135 		if (! defined $looppid) {
    136 			sleep 10;
    137 		} elsif ($looppid) {
    138 			wait;
    139 		} else {
    140 			exec $0, @ARGV;	
    141 			exit 1;
    142 		}
    143 		print STDERR "$program: re-starting service at ", scalar(localtime), " master-pid=$$\n";
    144 		sleep 1;
    145 	}
    146 	exit 0;
    147 }
    148 if (exists $ENV{INET6TO4_PIDFILE}) {
    149 	open_pidfile();
    150 }
    151 
    152 use IO::Socket::INET6;
    153 use strict;
    154 use warnings;
    155 
    156 # some settings:
    157 #
    158 my $verbose = 1;	# set to 0 for no messages, 2 for more.
    159 my $killpid = 1;	# does kill(2) at end of connection. 
    160 my $waittime = 0.25;	# time to wait between connections.
    161 my $reverse = 0;	# -r switch (or file named inet4to6)
    162 
    163 if (exists $ENV{INET6TO4_VERBOSE}) {
    164 	$verbose = $ENV{INET6TO4_VERBOSE};
    165 }
    166 if (exists $ENV{INET6TO4_WAITTIME}) {
    167 	$waittime = $ENV{INET6TO4_WAITTIME};
    168 }
    169 
    170 # process command line args:
    171 
    172 if (! @ARGV || $ARGV[0] =~ '^-+h') {	# -help
    173 	open(ME, "<$0");
    174 	while (<ME>) {
    175 		last unless /^#/;
    176 		next if /usr.bin.perl/;
    177 		$_ =~ s/# ?//;
    178 		print;
    179 	}
    180 	exit;
    181 }
    182 
    183 my $cmax = 0;
    184 if ($ARGV[0] eq '-c') {			# -c
    185 	shift;
    186 	$cmax = shift;
    187 }
    188 
    189 if ($ARGV[0] eq '-r') {			# -r
    190 	shift;
    191 	$reverse = 1;
    192 } elsif ($0 =~ /inet4to6$/) {
    193 	$reverse = 1;
    194 }
    195 
    196 my $listen_port = shift;		# ipv6-listen-port
    197 my $connect_to  = shift;		# ipv4-host:port
    198 
    199 die "no listen port or connect-to-host:port\n" if ! $listen_port || ! $connect_to;
    200 
    201 # connect to host:
    202 #
    203 my $host = '';
    204 my $port = '';
    205 if ($connect_to =~ /^(.*):(\d+)$/) {
    206 	$host = $1;
    207 	$port = $2;
    208 }
    209 die "invalid connect-to-host:port\n" if ! $host || ! $port;
    210 
    211 setpgrp(0, 0);
    212 
    213 # create listening socket:
    214 #
    215 my %opts;
    216 $opts{Listen}    = 10;
    217 $opts{Proto}     = "tcp";
    218 $opts{ReuseAddr} = 1;
    219 if ($listen_port =~ /^(.*):(\d+)$/) {
    220 	$opts{LocalAddr} = $1;
    221 	$listen_port = $2;
    222 }
    223 $opts{LocalPort} = $listen_port;
    224 
    225 if (!$reverse) {
    226 	# force ipv6 interface:
    227 	$opts{Domain} = AF_INET6;
    228 	$listen_sock = IO::Socket::INET6->new(%opts);
    229 } else {
    230 	$listen_sock = IO::Socket::INET->new(%opts);
    231 	if (! $listen_sock && $! =~ /invalid/i) {
    232 		warn "$program: $!, retrying with AF_UNSPEC:\n";
    233 		$opts{Domain} = AF_UNSPEC;
    234 		$listen_sock = IO::Socket::INET6->new(%opts);
    235 	}
    236 }
    237 if (! $listen_sock) {
    238 	die "$program: $!\n";
    239 }
    240 
    241 # for use by the xfer helper processes' interrupt handlers:
    242 #
    243 my $current_fh1 = '';
    244 my $current_fh2 = '';
    245 
    246 # connection counter:
    247 #
    248 my $conn = 0;
    249 
    250 # loop forever waiting for connections:
    251 #
    252 while (1) {
    253 	$conn++;
    254 	if ($cmax > 0 && $conn > $cmax) {
    255 		print STDERR "last connection ($cmax)\n" if $verbose; 
    256 		last;
    257 	}
    258 	print STDERR "listening for connection: $conn\n" if $verbose; 
    259 	my ($client, $ip) = $listen_sock->accept();
    260 
    261 	if ($client && !$reverse && $port == $listen_port) {
    262 		# This happens on Darwin 'tcp46'
    263 		if ($client->peerhost() =~ /^::ffff:/) {
    264 			print STDERR "closing client we think is actually us: ",
    265 			    $client->peerhost(), "\n";
    266 			close $client;
    267 			$client = undef;
    268 		}
    269 	}
    270 	if (! $client) {
    271 		# to throttle runaways
    272 		fsleep(2 * $waittime);
    273 		next;
    274 	}
    275 	print STDERR "conn: $conn -- ", $client->peerhost(), " at ", scalar(localtime), "\n" if $verbose;
    276 
    277 	# spawn helper:
    278 	#
    279 	my $pid = fork();
    280 	if (! defined $pid) {
    281 		die "$program: $!\n";
    282 	} elsif ($pid) {
    283 		wait;
    284 		# to throttle runaways
    285 		fsleep($waittime);
    286 		next;
    287 	} else {
    288 		# this is to avoid zombies:
    289 		close $listen_sock;
    290 		if (fork) {
    291 			exit 0;
    292 		}
    293 		setpgrp(0, 0);
    294 		handle_conn($client);
    295 	}
    296 }
    297 
    298 exit 0;
    299 
    300 sub handle_conn {
    301 	my $client = shift;
    302 
    303 	my $start = time();
    304 
    305 	print STDERR "connecting to: $host:$port\n" if $verbose;
    306 
    307 	my $sock = '';
    308 	my %opts;
    309 	$opts{PeerAddr} = $host;
    310 	$opts{PeerPort} = $port;
    311 	$opts{Proto}    = "tcp";
    312 	if (!$reverse) {
    313 		$sock = IO::Socket::INET->new(%opts);
    314 	} else {
    315 		$opts{Domain} = AF_INET6;
    316 		$sock = IO::Socket::INET6->new(%opts);
    317 	}
    318 	if (! $sock) {
    319 		warn "$program: $!, retrying with AF_UNSPEC:\n";
    320 		$opts{Domain} = AF_UNSPEC;
    321 		$sock = IO::Socket::INET6->new(%opts);
    322 	}
    323 
    324 	if (! $sock) {
    325 		close $client;
    326 		die "$program: $!\n";
    327 	}
    328 
    329 	$current_fh1 = $client;
    330 	$current_fh2 = $sock;
    331 
    332 	# interrupt handler:
    333 	#
    334 	$SIG{TERM} = sub {print STDERR "got sigterm\[$$]\n" if $verbose; close $current_fh1; close $current_fh2; exit 0};
    335 
    336 	# spawn another helper and transfer the data:
    337 	#
    338 	my $parent = $$;
    339 	if (my $child = fork()) {
    340 		xfer($sock, $client, 'S->C');
    341 		if ($killpid) {
    342 			fsleep(0.5);
    343 			kill 'TERM', $child;
    344 		}
    345 	} else {
    346 		xfer($client, $sock, 'C->S');
    347 		if ($killpid) {
    348 			fsleep(0.75);
    349 			kill 'TERM', $parent;
    350 		}
    351 	}
    352 
    353 	# done.
    354 	#
    355 	if ($verbose > 1) {
    356 		my $dt = time() - $start;
    357 		print STDERR "dt\[$$]: $dt\n";
    358 	}
    359 	exit 0;
    360 }
    361 
    362 # transfers data in one direction:
    363 #
    364 sub xfer {
    365 	my($in, $out, $lab) = @_;
    366 	my ($RIN, $WIN, $EIN, $ROUT);
    367 	$RIN = $WIN = $EIN = "";
    368 	$ROUT = "";
    369 	vec($RIN, fileno($in), 1) = 1;
    370 	vec($WIN, fileno($in), 1) = 1;
    371 	$EIN = $RIN | $WIN;
    372 	my $buf;
    373 
    374 	while (1) {
    375 		my $nf = 0;
    376 		while (! $nf) {
    377 			$nf = select($ROUT=$RIN, undef, undef, undef);
    378 		}
    379 		my $len = sysread($in, $buf, 8192);
    380 		if (! defined($len)) {
    381 			next if $! =~ /^Interrupted/;
    382 			print STDERR "$program\[$lab/$conn/$$]: $!\n";
    383 			last;
    384 		} elsif ($len == 0) {
    385 			print STDERR "$program\[$lab/$conn/$$]: "
    386 			    . "Input is EOF.\n";
    387 			last;
    388 		}
    389 
    390 		if ($verbose > 4) {
    391 			# verbose debugging of data:
    392 			syswrite(STDERR , "\n$lab: ", 6);
    393 			syswrite(STDERR , $buf, $len);
    394 		}
    395 
    396 		my $offset = 0;
    397 		my $quit = 0;
    398 		while ($len) {
    399 			my $written = syswrite($out, $buf, $len, $offset);
    400 			if (! defined $written) {
    401 				print STDERR "$program\[$lab/$conn/$$]: "
    402 				    . "Output is EOF. $!\n";
    403 				$quit = 1;
    404 				last;
    405 			}
    406 			$len -= $written;
    407 			$offset += $written;
    408 		}
    409 		last if $quit;
    410 	}
    411 	close($in);
    412 	close($out);
    413 }
    414 
    415 # sleep a fraction of a second:
    416 #
    417 sub fsleep {
    418 	my ($time) = @_;
    419 	select(undef, undef, undef, $time) if $time;
    420 }
    421