#!/usr/bin/perl -w

# Kevin Lahey, lahey@isi.edu
# July 11, 2007

# Set up ssh tunnel infrastructure for federation:
# 
# * Parse the configuration file provided.
#
# * Figure out whether we're the initiator or the reciever;  if we're 
#   the receiver, we just need to set up ssh keys and exit.
#
# * Pick out the experimental interface, remove the IP address
# 
# * Create a layer 2 ssh tunnel, set up bridging, and hang loose.

use strict;
use Getopt::Std;
use POSIX qw(strftime);
use Sys::Hostname;
use IO::File;

my $TMCC = "/usr/local/etc/emulab/tmcc";
my $SSH = "/usr/local/bin/ssh";
my $NC = "/usr/bin/nc";
my $SSH_PORT = 22;

sub setup_bridging;
sub setup_tunnel_cfg;
sub parse_config;

# Option use is as follows:
#    -f         filename containing config file
#    -d         turn on debugging output
#    -r		remotely invoked
#               (if remotely invoked the last two args are the tun interface
#               number and the remote address of the tunnel)

my $usage = "Usage: fed-tun.pl [-r] [-d] [-f config-filename] [count addr]\n";

my %opts;
my $filename;
my $remote;
my $debug = 1;
my $count;
my $addr;

if ($#ARGV != 0 && !getopts('df:r', \%opts)) {
    die "$usage";
}

if (defined($opts{'d'})) {
    $debug = 1;
}

if (defined($opts{'f'})) {
    $filename = $opts{'f'};
}

if (defined($opts{'r'})) {
    $remote = 1;
    die "$usage" if ($#ARGV != 1);
    
    $count = pop @ARGV;
    $addr = pop @ARGV;
}

die "$usage" if (!defined($remote) && !defined($filename));

if (defined($filename)) {
    &parse_config("$filename", \%opts) || 
	die "Cannot read config file $filename: $!\n";
}

my $ssh_port_fwds = "";

if (defined($opts{'fsname'})) {
	$ssh_port_fwds = "-R :139:$opts{'fsname'}:139 ";
}

if (defined($opts{'bossname'})) {
	$ssh_port_fwds .= "-R :7777:$opts{'bossname'}:7777 ";
}

$ssh_port_fwds = "" if ($opts{'type'} eq 'experiment');

print "ssh_port_fwds = $ssh_port_fwds\n" if ($debug);

print "opts %opts\n";

# Need these to make the Ethernet tap and bridge to work...

system("kldload /boot/kernel/if_bridge.ko");
system("kldload /boot/kernel/if_tap.ko");

if ($opts{'tunnelcfg'} && !$remote) {
    # Most Emulab-like testbeds use globally-routable addresses on the
    # control net;  at DETER, we isolate the control net from the Internet.
    # On DETER-like testbeds, we need to create special tunneling nodes
    # with external access.  Set up the external addresses as necessary.
    &setup_tunnel_cfg(%opts);
}

if (!$remote) {
    system("umask 077 && cp $opts{'privkeys'} /root/.ssh/id_rsa");
    system("umask 077 && cp $opts{'pubkeys'} /root/.ssh/id_rsa.pub");
    system("umask 077 && cat $opts{'pubkeys'} >> /root/.ssh/authorized_keys");
}

if ($opts{'active'}) {
    # If we're the initiator, open up a separate tunnel to the remote 
    # host for each of the different experiment net interfaces on this 
    # machine.  Execute this startup script on the far end, but with 
    # the -r option to indicate that it's getting invoked remotely and 
    # we should just handle the remote-end tasks.

    # Set up synchronization, so that the various user machines won't try to
    # contact boss before the tunnels are set up.  (Thanks jjh!)

    do {
	system("$NC -z $opts{'peer'} $SSH_PORT");
    } until (!$?);

    # XXX:  Do we need to clear out previously created bridge interfaces?

    my $count = 0;
    my @SSHCMD;

    open(IFFILE, "/var/emulab/boot/ifmap") || die "couldn't open ifmap\n";
    while (<IFFILE>) {
	my @a = split(' ');
	my $iface = $a[0];
	my $addr = $a[1];
	my $bridge = "bridge" . $count;
	my $tun = "tap" . $count;

	print "Found $iface, $addr, to bridge on $bridge\n" if ($debug);

	# Note that we're going to fire off an ssh which will never return;
	# we need the connection to stay up to keep the tunnel up.
	# In order to check for problems, we open it this way and read
	# the expected single line of output when the tunnel is connected.

	open($SSHCMD[$count], "$SSH -w $count:$count $ssh_port_fwds -o \"StrictHostKeyChecking no\" $opts{'peer'}  \"./fed-tun.pl -r $addr $count\" |") or die "Failed to run ssh";

	my $check = <$SSHCMD[$count]>;  # Make sure something ran...

	&setup_bridging($tun, $bridge, $iface, $addr);
	$count++;
	$ssh_port_fwds = "";  # only do this on the first connection
    }
    close(IFFILE);
} elsif ($remote) {
    # We're on the remote system;  for now, we just grab and use
    # the one experimental interface.  Later, we'll actually look
    # at the address passed to match up the appropriate interfaces.

    my $iter = 0;

    open(IFFILE, "/var/emulab/boot/ifmap") || die "couldn't open ifmap\n";
    while (<IFFILE>) {
	die "Argh, too many experimental interfaces!" if ($iter > 0);
	my @a = split(' ');
	my $iface = $a[0];
	my $addr = $a[1];
	my $bridge = "bridge" . $count;
	my $tun = "tap" . $count;

	&setup_bridging($tun, $bridge, $iface, $addr);
	$iter++;
    }
    close(IFFILE);

    print "Remote connection all set up!\n";  # Trigger other end with output
} else {
    print "inactive end of a connection, finishing" if ($debug);
}

print "all done!\n" if ($debug);
exit;


# Set up the bridging for the new stuff...

sub setup_bridging($; $; $; $) {
    my ($tun, $bridge, $iface, $addr) = @_;

    print "Got new $tun\n" if ($debug);
    print "setting up $bridge with $iface and nuking $addr\n" if ($debug);

    system("ifconfig $bridge create");
    system("ifconfig $iface delete $addr");
    system("ifconfig $bridge addm $iface up");
    system("ifconfig $bridge addm $tun");
}

# Set up tunnel info for DETER-like testbeds.

sub setup_tunnel_cfg {
    my (%opts) = @_;
    my $tunnel_iface = "em0";   # XXX
    my $tunnel_ip;
    my $tunnel_mask;
    my $tunnel_mac;
    my $tunnel_router;

    print "Opening $TMCC tunnelip\n" if ($debug);

    open(TMCD, "$TMCC tunnelip |") || die "tmcc failed\n";
    print "Opened $TMCC tunnelip\n" if ($debug);
    while (<TMCD>) {
        print "got one line from tmcc\n" if ($debug);
	print if ($debug);
	if (/^TUNNELIP=([0-9.]*) TUNNELMASK=([0-9.]*) TUNNELMAC=(\w*) TUNNELROUTER=([0-9.]*)$/) {
	    $tunnel_ip = $1;
	    $tunnel_mask = $2;
	    $tunnel_mac = $3;
	    $tunnel_router = $4;
	}
    }
    close(TMCD);

    die "Unable to determine tunnel node configuration information"
	if (!defined($tunnel_router));

    print "tunnel options:  ip=$tunnel_ip mask=$tunnel_mask mac=$tunnel_mac router=$tunnel_router\n" if ($debug);

    # Sadly, we ignore the tunnel mac for now -- we should eventually
    # use it to determine which interface to use, just like the
    # Emulab startup scripts.

    system("ifconfig $tunnel_iface $tunnel_ip" .
	   ($tunnel_mask ? " netmask $tunnel_mask" : ""));
    warn "configuration of tunnel interface failed" if ($?);
	
    system("route add $opts{'peer'} $tunnel_router");
    warn "configuration routes via tunnel interface failed" if ($?);

    print "setup_tunnel_cfg done\n" if ($debug);
}

# Trick config-file parsing code from Ted Faber:

# Parse the config file.  The format is a colon-separated parameter name
# followed by the value of that parameter to the end of the line.  This parses
# that format and puts the parameters into the referenced hash.  Parameter
# names are mapped to lower case, parameter values are unchanged.  Returns 0 on
# failure (e.g. file open) and 1 on success.
sub parse_config {
    my($file, $href) = @_;
    my($fh) = new IO::File($file);
	
    unless ($fh) {
	warn "Can't open $file: $!\n";
	return 0;
    }

    while (<$fh>) {
	next if /^\s*#/ || /^\s*$/;	# Skip comments & blanks
	chomp;
	/^([^:]+):\s*(.*)/ && do {
	    my($key) = $1; 

	    $key =~ tr/A-Z/a-z/;
	    $href->{$key} = $2;
	    next;
	};
	warn "Unparasble line in $file: $_\n";
    }
    $fh->close();   # It will close when it goes out of scope, but...
    return 1;
}
