#!/usr/bin/perl
#
# inet6to4:  Act as an ipv6-to-ipv4 relay for tcp applications that
#            do not support ipv6.
#
# Usage:     inet6to4    <ipv6-listen-port> <ipv4-host:port>
#            inet6to4 -r <ipv4-listen-port> <ipv6-host:port>
#
# Examples:  inet6to4 5900 localhost:5900
#            inet6to4 8080 web1:80
#            inet6to4 -r 5900 fe80::217:f2ff:fee6:6f5a%eth0:5900
#
# The -r option reverses the direction of translation (e.g. for ipv4
# clients that need to connect to ipv6 servers.)  Reversing is the default
# if this script is named 'inet4to6' (e.g. by a symlink.)
#
# Use Ctrl-C to stop this program.  You can also supply '-c n' as the
# first option to only handle that many connections.
#
# Also set the env. vars INET6TO4_LOOP=1 or INET6TO4_LOOP=BG
# to have an outer loop restarting this program (BG means do that
# in the background), and INET6TO4_LOGFILE for a log file.
# Also set INET6TO4_VERBOSE to verbosity level and INET6TO4_WAITTIME
# and INET6TO4_PIDFILE (see below.)
#

#-------------------------------------------------------------------------
# Copyright (c) 2010 by Karl J. Runge <runge@karlrunge.com>
#
# inet6to4 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.
# 
# inet6to4 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 inet6to4; if not, write to the Free Software
# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA
# or see <http://www.gnu.org/licenses/>.
#-------------------------------------------------------------------------

my $program = "inet6to4";

# Set up logging:
#
if (exists $ENV{INET6TO4_LOGFILE}) {
	close STDOUT;
	if (!open(STDOUT, ">>$ENV{INET6TO4_LOGFILE}")) {
	        die "$program: $ENV{INET6TO4_LOGFILE} $!\n";
	}
	close STDERR;
	open(STDERR, ">&STDOUT");
}
select(STDERR); $| = 1;
select(STDOUT); $| = 1;

# interrupt handler:
#
my $looppid = '';
my $pidfile = '';
my $listen_sock = '';	# declared here for get_out()
#
sub get_out {
	print STDERR "$_[0]:\t$$ looppid=$looppid\n";
	close $listen_sock if $listen_sock;
	if ($looppid) {
		kill 'TERM', $looppid;
		fsleep(0.2);
	}
	unlink $pidfile if $pidfile;
	exit 0;
}
$SIG{INT}  = \&get_out;
$SIG{TERM} = \&get_out;

# pidfile:
#
sub open_pidfile {
	if (exists $ENV{INET6TO4_PIDFILE}) {
		my $pf = $ENV{INET6TO4_PIDFILE};
		if (open(PID, ">$pf")) {
			print PID "$$\n";
			close PID;
			$pidfile = $pf;
		} else {
			print STDERR "could not open pidfile: $pf - $! - continuing...\n";
		}
		delete $ENV{INET6TO4_PIDFILE};
	}
}

####################################################################
# Set INET6TO4_LOOP=1 to have this script create an outer loop
# restarting itself if it ever exits.  Set INET6TO4_LOOP=BG to
# do this in the background as a daemon.

if (exists $ENV{INET6TO4_LOOP}) {
	my $csl = $ENV{INET6TO4_LOOP};
	if ($csl ne 'BG' && $csl ne '1') {
		die "$program: invalid INET6TO4_LOOP.\n";
	}
	if ($csl eq 'BG') {
		# go into bg as "daemon":
		setpgrp(0, 0);
		my $pid = fork();
		if (! defined $pid) {
			die "$program: $!\n";
		} elsif ($pid) {
			wait;
			exit 0;
		}
		if (fork) {
			exit 0;
		}
		setpgrp(0, 0);
		close STDIN;
		if (! $ENV{INET6TO4_LOGFILE}) {
			close STDOUT;
			close STDERR;
		}
	}
	delete $ENV{INET6TO4_LOOP};

	if (exists $ENV{INET6TO4_PIDFILE}) {
		open_pidfile();
	}

	print STDERR "$program: starting service at ", scalar(localtime), " master-pid=$$\n";
	while (1) {
		$looppid = fork;
		if (! defined $looppid) {
			sleep 10;
		} elsif ($looppid) {
			wait;
		} else {
			exec $0, @ARGV;	
			exit 1;
		}
		print STDERR "$program: re-starting service at ", scalar(localtime), " master-pid=$$\n";
		sleep 1;
	}
	exit 0;
}
if (exists $ENV{INET6TO4_PIDFILE}) {
	open_pidfile();
}

use IO::Socket::INET6;
use strict;
use warnings;

# some settings:
#
my $verbose = 1;	# set to 0 for no messages, 2 for more.
my $killpid = 1;	# does kill(2) at end of connection. 
my $waittime = 0.25;	# time to wait between connections.
my $reverse = 0;	# -r switch (or file named inet4to6)

if (exists $ENV{INET6TO4_VERBOSE}) {
	$verbose = $ENV{INET6TO4_VERBOSE};
}
if (exists $ENV{INET6TO4_WAITTIME}) {
	$waittime = $ENV{INET6TO4_WAITTIME};
}

# process command line args:

if (! @ARGV || $ARGV[0] =~ '^-+h') {	# -help
	open(ME, "<$0");
	while (<ME>) {
		last unless /^#/;
		next if /usr.bin.perl/;
		$_ =~ s/# ?//;
		print;
	}
	exit;
}

my $cmax = 0;
if ($ARGV[0] eq '-c') {			# -c
	shift;
	$cmax = shift;
}

if ($ARGV[0] eq '-r') {			# -r
	shift;
	$reverse = 1;
} elsif ($0 =~ /inet4to6$/) {
	$reverse = 1;
}

my $listen_port = shift;		# ipv6-listen-port
my $connect_to  = shift;		# ipv4-host:port

die "no listen port or connect-to-host:port\n" if ! $listen_port || ! $connect_to;

# connect to host:
#
my $host = '';
my $port = '';
if ($connect_to =~ /^(.*):(\d+)$/) {
	$host = $1;
	$port = $2;
}
die "invalid connect-to-host:port\n" if ! $host || ! $port;

setpgrp(0, 0);

# create listening socket:
#
my %opts;
$opts{Listen}    = 10;
$opts{Proto}     = "tcp";
$opts{ReuseAddr} = 1;
if ($listen_port =~ /^(.*):(\d+)$/) {
	$opts{LocalAddr} = $1;
	$listen_port = $2;
}
$opts{LocalPort} = $listen_port;

if (!$reverse) {
	# force ipv6 interface:
	$opts{Domain} = AF_INET6;
	$listen_sock = IO::Socket::INET6->new(%opts);
} else {
	$listen_sock = IO::Socket::INET->new(%opts);
	if (! $listen_sock && $! =~ /invalid/i) {
		warn "$program: $!, retrying with AF_UNSPEC:\n";
		$opts{Domain} = AF_UNSPEC;
		$listen_sock = IO::Socket::INET6->new(%opts);
	}
}
if (! $listen_sock) {
	die "$program: $!\n";
}

# for use by the xfer helper processes' interrupt handlers:
#
my $current_fh1 = '';
my $current_fh2 = '';

# connection counter:
#
my $conn = 0;

# loop forever waiting for connections:
#
while (1) {
	$conn++;
	if ($cmax > 0 && $conn > $cmax) {
		print STDERR "last connection ($cmax)\n" if $verbose; 
		last;
	}
	print STDERR "listening for connection: $conn\n" if $verbose; 
	my ($client, $ip) = $listen_sock->accept();

	if ($client && !$reverse && $port == $listen_port) {
		# This happens on Darwin 'tcp46'
		if ($client->peerhost() =~ /^::ffff:/) {
			print STDERR "closing client we think is actually us: ",
			    $client->peerhost(), "\n";
			close $client;
			$client = undef;
		}
	}
	if (! $client) {
		# to throttle runaways
		fsleep(2 * $waittime);
		next;
	}
	print STDERR "conn: $conn -- ", $client->peerhost(), " at ", scalar(localtime), "\n" if $verbose;

	# spawn helper:
	#
	my $pid = fork();
	if (! defined $pid) {
		die "$program: $!\n";
	} elsif ($pid) {
		wait;
		# to throttle runaways
		fsleep($waittime);
		next;
	} else {
		# this is to avoid zombies:
		close $listen_sock;
		if (fork) {
			exit 0;
		}
		setpgrp(0, 0);
		handle_conn($client);
	}
}

exit 0;

sub handle_conn {
	my $client = shift;

	my $start = time();

	print STDERR "connecting to: $host:$port\n" if $verbose;

	my $sock = '';
	my %opts;
	$opts{PeerAddr} = $host;
	$opts{PeerPort} = $port;
	$opts{Proto}    = "tcp";
	if (!$reverse) {
		$sock = IO::Socket::INET->new(%opts);
	} else {
		$opts{Domain} = AF_INET6;
		$sock = IO::Socket::INET6->new(%opts);
	}
	if (! $sock) {
		warn "$program: $!, retrying with AF_UNSPEC:\n";
		$opts{Domain} = AF_UNSPEC;
		$sock = IO::Socket::INET6->new(%opts);
	}

	if (! $sock) {
		close $client;
		die "$program: $!\n";
	}

	$current_fh1 = $client;
	$current_fh2 = $sock;

	# interrupt handler:
	#
	$SIG{TERM} = sub {print STDERR "got sigterm\[$$]\n" if $verbose; close $current_fh1; close $current_fh2; exit 0};

	# spawn another helper and transfer the data:
	#
	my $parent = $$;
	if (my $child = fork()) {
		xfer($sock, $client, 'S->C');
		if ($killpid) {
			fsleep(0.5);
			kill 'TERM', $child;
		}
	} else {
		xfer($client, $sock, 'C->S');
		if ($killpid) {
			fsleep(0.75);
			kill 'TERM', $parent;
		}
	}

	# done.
	#
	if ($verbose > 1) {
		my $dt = time() - $start;
		print STDERR "dt\[$$]: $dt\n";
	}
	exit 0;
}

# transfers data in one direction:
#
sub xfer {
	my($in, $out, $lab) = @_;
	my ($RIN, $WIN, $EIN, $ROUT);
	$RIN = $WIN = $EIN = "";
	$ROUT = "";
	vec($RIN, fileno($in), 1) = 1;
	vec($WIN, fileno($in), 1) = 1;
	$EIN = $RIN | $WIN;
	my $buf;

	while (1) {
		my $nf = 0;
		while (! $nf) {
			$nf = select($ROUT=$RIN, undef, undef, undef);
		}
		my $len = sysread($in, $buf, 8192);
		if (! defined($len)) {
			next if $! =~ /^Interrupted/;
			print STDERR "$program\[$lab/$conn/$$]: $!\n";
			last;
		} elsif ($len == 0) {
			print STDERR "$program\[$lab/$conn/$$]: "
			    . "Input is EOF.\n";
			last;
		}

		if ($verbose > 4) {
			# verbose debugging of data:
			syswrite(STDERR , "\n$lab: ", 6);
			syswrite(STDERR , $buf, $len);
		}

		my $offset = 0;
		my $quit = 0;
		while ($len) {
			my $written = syswrite($out, $buf, $len, $offset);
			if (! defined $written) {
				print STDERR "$program\[$lab/$conn/$$]: "
				    . "Output is EOF. $!\n";
				$quit = 1;
				last;
			}
			$len -= $written;
			$offset += $written;
		}
		last if $quit;
	}
	close($in);
	close($out);
}

# sleep a fraction of a second:
#
sub fsleep {
	my ($time) = @_;
	select(undef, undef, undef, $time) if $time;
}
