#!/usr/bin/perl -w

# mad-setup.pl
# Copyright (C) Etienne Zind 2008 <etienne.zind@gmail.com>
#  
# This program 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 3 of the License, or
# (at your option) any later version.
#  
# This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#

use strict;

my $ip =`which ip`;
chomp $ip;
die("Fatal Error: the `ip` program was not found.")
    unless $ip;

my $ifconfig =`which ifconfig`;
chomp $ifconfig;
die("Fatal Error: the `ifconfig` program was not found.")
    unless $ifconfig;

my $nm_tool =`which nm-tool`;
chomp $nm_tool;

use Getopt::Long;
use Pod::Usage;

my @opt_device       = ();
my $opt_reset        = 0;
my $opt_all_up       = 0;
my $opt_default_dev  = "";
my $opt_verbose      = 0;
my $opt_version      = 0;
my $opt_simulate     = 0;
my $opt_interactive  = 1;
my $opt_tables_file  = "/etc/iproute2/rt_tables";
my $opt_resolv_file  = "/etc/resolv.conf";
my $opt_balance_load = 0;
my $opt_help         = 0;
my $opt_man          = 0;

GetOptions (
    "device=s"         => \@opt_device,
    "reset"            => \$opt_reset,
    "all-up"           => \$opt_all_up,
    "default-device=s" => \$opt_default_dev,
    "verbose"          => \$opt_verbose,
    "simulate"         => \$opt_simulate,
    "balance-load!"    => \$opt_balance_load,
    "interactive!"     => \$opt_interactive,
    "tables-file=s"    => \$opt_tables_file,
    "resolve-file=s"   => \$opt_resolv_file,
    "help"             => \$opt_help,
    "man"              => \$opt_man,
    "version"          => \$opt_version,
    ) or pod2usage(2);
pod2usage(1) if $opt_help;
pod2usage(-exitstatus => 0, -verbose => 2) if $opt_man;

if ( $opt_version ) {
    print "mad-setup.pl v.1.0\n";
    exit(0);
}

$opt_verbose = 1
    if $opt_simulate;

#
# doing the boring job of getting all the info on devices (ip/gateway/DNSs...)
#

my %devices = ();
my @devices = ();

#
# parsing the --device options
# they should look like that :
# --device eth0:ip=XXX.XXX.XXX.XX:gw=YYY.YYY.YYY.YYY:mask=...:dns=AAA.AAA.AAA.AAA,BBB.BBB.BBB.BBB
#
# the first part MUST be the device's name
# the posible keys are ip,gw,mask,dns.weight
#
foreach my $dev ( @opt_device ) {
    my %dev = map { /=/ ? split(/=/, $_) : ($_,1) ; } split(/:/, "name=" . $dev);
    $dev{dns} = [ split(/,/, $dev{dns})]
	if ( exists $dev{dns} );
    $devices{$dev{name}} = @devices - 0; 
    push(@devices, \%dev);
}

#
# if the --all-up options was on or if no --device as been specified, we try to get 
# all devices possible
#
if ( $opt_all_up || @devices == 0 ) {
    my @ifconfig_output = `$ifconfig`;
    foreach ( @ifconfig_output ) {
	if ( /^(\w+)\s+Link encap:(Ethernet|Point-to-Point Protocol)/ ) {
	    if ( ! exists $devices{$1} ) {
		$devices{$1} = @devices - 0; 
		push(@devices, {name => $1, _auto => 1});  
	    }
	}
    }
}

#
# if we have the nm-tool program (gnome NetworkManager), we use it
# to get Gateways and DNS servers
#
my @nmtool_output = $nm_tool ? `$nm_tool` : ();
my %nmtool_info = ();
my $nmtool_address;
my $nmtool_default;
foreach ( @nmtool_output ) {
    if ( /^- Device:/ ) {
	$nmtool_address = "";
	$nmtool_default = 0;
    }
    elsif ( /^\s*Address:\s*(\d+\.\d+\.\d+\.\d+)/ ) {
	$nmtool_address = $1;
	$nmtool_info{$nmtool_address} = {mask => "", gw => "", dns => [], default => $nmtool_default};
    }
    elsif ( /^\s*Default:\s*(yes|no)/ ) {
	$nmtool_default = ($1 eq "yes") ? 1 : 0;
    }
    elsif ( ! $nmtool_address ) {
	next;
    }
    elsif ( /^\s*Prefix:\s*\d+\s+\((\d+\.\d+\.\d+\.\d+)\)/ ) {
	$nmtool_info{$nmtool_address}->{mask} = $1;
    }
    elsif ( /^\s*Gateway:\s*(\d+\.\d+\.\d+\.\d+)/ ) {
	$nmtool_info{$nmtool_address}->{gw} = $1;
    }
    elsif ( /^\s*DNS:\s*(\d+\.\d+\.\d+\.\d+)/ ) {
	push(@{$nmtool_info{$nmtool_address}->{dns}}, $1);
    }
}

#
# now we keep only the devices that seem to have a INET address
# and build up the final @devices list
#
my @devices_up;
my %devices_up;
my $nmtool_default_dev = "";
my $nameserver_count = 0;
foreach ( @devices ) {
    my %dev = %$_;

    my @ifconfig_output = `$ifconfig $dev{name}`;
    my @ppp_inet = grep /inet addr:\d+\.\d+\.\d+\.\d+  P-t-P:\d+\.\d+\.\d+\.\d+  Mask:\d+\.\d+\.\d+\.\d+/, @ifconfig_output;
    my @eth_inet = grep /inet addr:\d+\.\d+\.\d+\.\d+  Bcast:\d+\.\d+\.\d+\.\d+  Mask:\d+\.\d+\.\d+\.\d+/, @ifconfig_output;
    
    if ( @ppp_inet ) {
	$ppp_inet[0]  =~ /inet addr:(\d+\.\d+\.\d+\.\d+)  P-t-P:(\d+\.\d+\.\d+\.\d+)  Mask:(\d+\.\d+\.\d+\.\d+)/;
	$dev{ip}   = $1;
	$dev{gw}   = $2;
	$dev{mask} = $3;
    }
    elsif ( @eth_inet ) {
	$eth_inet[0]  =~ /inet addr:(\d+\.\d+\.\d+\.\d+)  Bcast:\d+\.\d+\.\d+\.\d+  Mask:(\d+\.\d+\.\d+\.\d+)/;
	$dev{ip}   = $1;
	$dev{mask} = $2;
    }
    else {
	print STDERR "Warning: Device '$dev{name}' does not have any IP address configured. Dropped.\n"
	    unless $_->{_auto};
	next;
    }

    $nmtool_default_dev = $dev{name} if (exists $nmtool_info{$dev{ip}} && exists $nmtool_info{$dev{ip}}{default});

    $dev{gw} = $nmtool_info{$dev{ip}}{gw} if ( exists $nmtool_info{$dev{ip}});

    $dev{dns} = $nmtool_info{$dev{ip}}{dns} if ( exists $nmtool_info{$dev{ip}} );

    #
    # for gateways and DNS we allow interactive setup just in case we are
    # not using nm-tool
    # TODO: ISP catalog => --device eth0:isp=MyISP
    # 

    if ( !$dev{gw} ) {
	if ( $opt_interactive ) {
	    print "Device '$dev{name}' does not have any gateway address configured.\n";
	    print "'$dev{name}' gateway address:\n";
	    $dev{gw} = <STDIN>;
	    chomp $dev{gw};
	}
	else {
	    print STDERR "Warning: Device '$dev{name}' does not have any gateway address configured. Dropped.\n";
	    next;		
	}
    }

    $dev{net} = _make_net($dev{gw}, $dev{mask});

    if ( !$dev{dns} || @{$dev{dns}} == 0 ) {
	if ( $opt_interactive ) {
	    print "Device '$dev{name}' does not have any DNS server configured.\n";
	    print "'$dev{name}' DNS server addresses (comma separated):\n";
	    $dev{dns} = <STDIN>;
	    $dev{dns} =~ s/\s+//g;
	    $dev{dns} = [split(/,/, $dev{dns})];
	}
	elsif ( $opt_verbose ) {
	    print "Device '$dev{name}' does not have any DNS server configured.\n";
	}
    }

    $nameserver_count += @{$dev{dns}};

    if ( exists $dev{weight} ) {
	$opt_balance_load = 1;
    }
    else {
	$dev{weight} = 1;
    }

    $devices_up{$dev{name}} = @devices_up - 0;
    push @devices_up, \%dev;
}
@devices = @devices_up;
%devices = %devices_up;

die("Fatal Error: no namserver configured\n")
    unless ( $nameserver_count );


if ( $opt_verbose ) {
    print "Setting up devices:\n";
    foreach my $dev ( @devices ) {
	print " - $dev->{name} -------\n";
	print "     ip:      $dev->{ip}\n";
	print "     gateway: $dev->{gw}\n";
	print "     net:     $dev->{net}\n";
	print "     weight:  $dev->{weight}\n"
	    if ( exists $dev->{weight} );
	print "     dns:     $_\n"
	    foreach ( @{$dev->{dns}} );
    }
}

# fix up the main default route device, if none then we load balance
my $main_default_dev = "";
if ( !$opt_balance_load ) {
    $main_default_dev = ($opt_default_dev && exists $devices{$opt_default_dev}) ? $opt_default_dev : $nmtool_default_dev;
}
if ( $opt_verbose ) {
    print $main_default_dev ? "Default route device `$main_default_dev`\n" : "No default route device: load balancing";
}

#
# clean up all routing / resolv material
#


print "Flushing all routing rules\n"
    if ( $opt_verbose );
_exec("$ip rule flush");
_exec("$ip rule add priority 32766 from all lookup main");
_exec("$ip rule add priority 32767 from all lookup default");

print "Flushing all routing tables\n"
    if ( $opt_verbose );
_exec("$ip route flush table main")
    if(`$ip route show table main`);
_exec("$ip route flush table default")
    if(`$ip route show table default`);
foreach ( 1 .. 252 ) {
    _exec("$ip route flush table $_")
	if ( `$ip route show table $_` );
}
_write($opt_tables_file,"#
# reserved values
#
255     local
254     main
253     default
0       unspec
#
# local
#
");

print "Reseting namservers\n"
    if ( $opt_verbose );
_write($opt_resolv_file,"");

exit 0 
    if $opt_reset;

#
# configuration
#

# list of table IDs to be added in $opt_table_file
my @tables_file_contents = ("1\tdns\n");
# dns nameservers, to be added to $resolv_file
my @resolv_file_contents = ();

# list of rules; starting by trying dns routingtable
my @rules  = ("priority 1 from all table dns");

# list of route to be added
my %routes  = ();
my @tables  = ("main","dns");
$routes{main} = [];
$routes{dns} = [];

foreach ( @devices ) {

    my $table = "dev_" . $_->{name};

    # first we add a routing table for this device
    push ( @tables_file_contents, ( @tables_file_contents + 1 ) . "\t$table\n" );
    push ( @tables, $table );
    $routes{$table} = [];

    # then a rule that forces the use of this table for trafic with this device's IP
    push ( @rules, "priority " . ( @rules + 1 ) . " from " . $_->{ip} . " table dev_" . $_->{name} );

    # add a route for packets with a target in this devices network, on both main and this device's table
    push ( @{$routes{$table}}, $_->{net} . " dev " . $_->{name} . " src " . $_->{ip} );
    push ( @{$routes{main}}, $_->{net} . " dev " . $_->{name} . " src " . $_->{ip} );

    # we add the dns server of this devices if there are some, and create the route to those in the dns table
    if ( @{$_->{dns}} ) {
	foreach my $dns ( @{$_->{dns}} ) {
	    push ( @{$routes{dns}}, $dns . " dev " . $_->{name} . " src " . $_->{ip});
	    push ( @resolv_file_contents, "nameserver $dns\n" );
	}
    }

    # we add the routes to the other devices' networks on this device's table
    push(@{$routes{$table}}, "127.0.0.0/255.0.0.0 dev lo src 127.0.0.1");
    foreach my $dev2 ( @devices ) {
	push ( @{$routes{$table}}, $dev2->{net} . " dev " . $dev2->{name} . " src " . $dev2->{ip} )
	    if ( $_->{name} ne $dev2->{name} );
    }

    # we add the default route for this device's table, which is through this device
    push ( @{$routes{$table}}, "default via " . $_->{gw} . " src " . $_->{ip});
    
}


#
# main table default route ( default dev, load balancing ? )
#
my $multipath = "";
if ( @devices ) {
    
    if ( (@devices == 1) || $main_default_dev ) {
	push ( @{$routes{main}}, "default via " . $devices[$devices{$main_default_dev}]->{gw} );

    }
    else {
	$multipath = "$ip route add default scope global table main";
	$multipath .= " nexthop via " . $_->{gw} . " dev " . $_->{name} . " weight " . $_->{weight} foreach ( @devices );
    }
}


# we update $opt_tables_file with all our table ids
_owrite($opt_tables_file, join("", @tables_file_contents));

# we update $opt_resolv_file with all our namservers
_owrite($opt_resolv_file, join("", @resolv_file_contents));

# we add our routes
foreach my $table ( @tables ) {
    foreach my $route ( @{$routes{$table}} ) {
	_exec("$ip route add $route table $table");
    }
}
_exec($multipath)
    if ( $multipath );

# we add our rules
foreach my $rule ( @rules ) {
    _exec("$ip rule add $rule");
}




sub _make_net {
    my ($ip, $mask) = @_;
    my @ip = split(/\./, $ip);
    my @mask = split(/\./, $mask);
    my @net = ();

    foreach my $i ( 0 .. 3 ) {
	my $ipbyte = $ip[$i] - 0;
	my $maskbyte = $mask[$i] - 0;
	my $netbyte = $ipbyte & $maskbyte;
	push(@net, $netbyte);
    }

    return join(".", @net) . "/" . $mask;
}

sub _exec {
    if ( $opt_simulate ) {
	printf '$ ' . $_[0] . "\n";
    }
    else {
	printf '$ ' . $_[0] . "\n"
	    if ( $opt_verbose );
	return `$_[0]`;
    }
}

sub _write {
    if ( $opt_simulate ) {
	printf "\n --- begin $_[0] ---\n$_[1] --- end $_[0] ---\n\n";
    }
    else {
	open(FILE, ">$_[0]")
	    or die ( "Could not write file `$_[0]`: $!" );
	print FILE $_[1];
	close(FILE);
    }
}

sub _owrite {
    if ( $opt_simulate ) {
	printf "\n --- append $_[0]  ---\n$_[1] --- end $_[0] ---\n\n";
    }
    else {
	open(FILE, ">>$_[0]")
	    or die ( "Could not write file `$_[0]`: $!" );
	print FILE $_[1];
	close(FILE);
    }
}


__END__
    
=head1 NAME
    
mad-setup.pl - setup full routing and resolving for Multiple Access Device
    
=head1 SYNOPSIS
    
mad-setup.pl [options]
    
=head1 OPTIONS

=over 8

=item B<--all-up>

=item B<--balance-load>

=item B<--default-device> devname

=item B<--device> devname[:key=value[...]]

=item B<--help>

=item B<--interactive>

=item B<--man>

=item B<--reset>

=item B<--resolve-file> filename

=item B<--simulate>

=item B<--tables-file> filename

=item B<--verbose>


