#
# (c) Jan Gehring <jan.gehring@gmail.com>
# 
# vim: set ts=3 sw=3 tw=0:
# vim: set expandtab:
   
=head1 NAME

Rex::Commands::Iptables - Iptable Management Commands

=head1 DESCRIPTION

With this Module you can manage basic Iptables rules.

=head1 SYNOPSIS

 use Rex::Commands::Iptables;
     
 task "firewall", sub {
    iptables_clear;
     
    open_port 22;
    open_port [22, 80] => {
       dev => "eth0",
    };
        
    close_port 22 => {
       dev => "eth0",
    };
    close_port "all";
        
    redirect_port 80 => 10080;
    redirect_port 80 => {
       dev => "eth0",
       to  => 10080,
    };
      
    default_state_rule;
    default_state_rule dev => "eth0";
        
    is_nat_gateway;
       
    iptables t => "nat",
             A => "POSTROUTING",
             o => "eth0",
             j => "MASQUERADE";
    
 };

=head1 EXPORTED FUNCTIONS

=over 4

=cut

package Rex::Commands::Iptables;

use strict;
use warnings;

require Rex::Exporter;
use Data::Dumper;

use base qw(Rex::Exporter);

use vars qw(@EXPORT);

use Rex::Commands::Sysctl;
use Rex::Commands::Run;
use Rex::Commands::Gather;

use Rex::Logger;

@EXPORT = qw(iptables is_nat_gateway iptables_list iptables_clear 
               open_port close_port redirect_port
               default_state_rule);

sub iptables;

=item open_port($port, $option)

Open a port for inbound connections.

 task "firewall", sub {
    open_port 22;
    open_port [22, 80];
    open_port [22, 80] => { dev => "eth1", };
 };

=cut
sub open_port {

   my ($port, $option) = @_;
   _open_or_close_port("i", "I", "INPUT", "ACCEPT", $port, $option);

}

=item close_port($port, $option)

Close a port for inbound connections.

 task "firewall", sub {
    close_port 22;
    close_port [22, 80];
    close_port [22, 80] => { dev => "eth0", };
 };

=cut
sub close_port {

   my ($port, $option) = @_;
   _open_or_close_port("i", "A", "INPUT", "DROP", $port, $option);

}

=item redirect_port($in_port, $option)

Redirect $in_port to an other local port.

 task "redirects", sub {
    redirect_port 80 => 10080;
    redirect_port 80 => {
       to  => 10080,
       dev => "eth0",
    };
 };

=cut
sub redirect_port {
   my ($in_port, $option) = @_;

   my @opts;

   push (@opts, "t", "nat");

   if(! ref($option)) {
      my $net_info = network_interfaces();
      my @devs = keys %{$net_info};

      for my $dev (@devs) {
         redirect_port($in_port, {
            dev => $dev,
            to  => $option,
         });
      }
      
      return;
   }

   unless(exists $option->{"dev"}) {
      my $net_info = network_interfaces();
      my @devs = keys %{$net_info};

      for my $dev (@devs) {
         $option->{"dev"} = $dev;
         redirect_port($in_port, $option);
      }

      return;
   }

   if($option->{"to"} =~ m/^\d+$/) {
      $option->{"proto"} ||= "tcp";

      push(@opts, "I", "PREROUTING", "i", $option->{"dev"}, "p", $option->{"proto"}, "m", $option->{"proto"});
      push(@opts, "dport", $in_port, "j", "REDIRECT", "to-ports", $option->{"to"});

   }
   else {
      Rex::Logger::info("Redirect to other hosts isn't supported right now. Please do it by hand.");
   }

   iptables @opts;
}

=item iptables(@params)

Write standard iptable comands.

 task "firewall", sub {
    iptables t => "nat", A => "POSTROUTING", o => "eth0", j => "MASQUERADE";
    iptables t => "filter", i => "eth0", m => "state", state => "RELATED,ESTABLISHED", j => "ACCEPT";
        
    iptables "flush";
    iptables -F;
    iptables flush => "filter";
    iptables -F => "filter";
 };

=cut
sub iptables {
   my (@params) = @_;

   if($params[0] eq "flush" || $params[0] eq "-flush" || $params[0] eq "-F") {
      if($params[1]) {
         run "iptables -F -t $params[1]";
      }
      else {
         run "iptables -F";
      }

      return;
   }

   my $cmd = "";
   my $n = -1;
   while( $params[++$n] ) {
      my ($key, $val) = reverse @params[$n, $n++];

      if(ref($key) eq "ARRAY") {
         $cmd .= join(" ", @{$key});
         last;
      }

      if(length($key) == 1) {
         $cmd .= "-$key $val ";
      }
      else {
         $cmd .= "--$key $val ";
      }
   }

   if(can_run("iptables")) {
      run "iptables $cmd";

      if($? != 0) {
         Rex::Logger::info("Error setting iptable rule: $cmd", "warn");
         die("Error setting iptable rule: $cmd");
      }
   }
   else {
      Rex::Logger::info("IPTables not found.");
      die("IPTables not found.");
   }
}

=item is_nat_gateway

This function create a nat gateway for the device the default route points to.

 task "make-gateway", sub {
    is_nat_gateway;
 };

=cut
sub is_nat_gateway {

   Rex::Logger::debug("Changing this system to a nat gateway.");

   if(can_run("ip")) {

      my @iptables_option = ();

      my ($default_line) = run "/sbin/ip r |grep ^default";
      my ($dev) = ($default_line =~ m/dev ([a-z0-9]+)/i);
      Rex::Logger::debug("Default GW Device is $dev");

      sysctl "net.ipv4.ip_forward" => 1;
      iptables t => "nat", A => "POSTROUTING", o => $dev, j => "MASQUERADE";

   }
   else {

      Rex::Logger::info("No /sbin/ip found.");

   }

}

=item default_state_rule(%option)

Set the default state rules for the given device.

 task "firewall", sub {
    default_state_rule(dev => "eth0");
 };

=cut
sub default_state_rule {
   my (%option) = @_;

   unless(exists $option{"dev"}) {
      my $net_info = network_interfaces();
      my @devs = keys %{$net_info};

      for my $dev (@devs) {
         default_state_rule(dev => $dev);
      }

      return;
   }

   iptables t => "filter", A => "INPUT", i => $option{"dev"}, m => "state", state => "RELATED,ESTABLISHED", j => "ACCEPT";
}

=item iptables_list

List all iptables rules.

 task "list-iptables", sub {
    print Dumper iptables_list;
 };

=cut
sub iptables_list {
   my @lines = run "/sbin/iptables-save";
   _iptables_list(@lines);
}
   
sub _iptables_list {
   my (%tables, $ret);
   my (@lines) = @_;

   my ($current_table);
   for my $line (@lines) {
      chomp $line;

      next if($line eq "COMMIT");
      next if($line =~ m/^#/);
      next if($line =~ m/^:/);

      if($line =~ m/^\*([a-z]+)$/) {
         $current_table = $1;
         $tables{$current_table} = [];
         next;
      }

      #my @parts = grep { ! /^\s+$/ && ! /^$/ } split (/(\-\-?[^\s]+\s[^\s]+)/i, $line);
      my @parts = grep { ! /^\s+$/ && ! /^$/ } split (/^\-\-?|\s+\-\-?/i, $line); 

      my @option = ();
      for my $part (@parts) {
         my ($key, $value) = split(/\s/, $part, 2);
         #$key =~ s/^\-+//;
         push(@option, $key => $value);
      }

      push (@{$ret->{$current_table}}, \@option);

   }

   return $ret;
}

=item iptables_clear

Remove all iptables rules.

 task "no-firewall", sub {
    iptables_clear;
 };

=cut
sub iptables_clear {

   for my $table (qw/nat mangle filter/) {
      iptables t => $table, F => '';
      iptables t => $table, X => '';
   }

   for my $p (qw/INPUT FORWARD OUTPUT/) {
      iptables P => $p, ["ACCEPT"];
   }

}

sub _open_or_close_port {
   my ($dev_type, $push_type, $chain, $jump, $port, $option) = @_;

   my @opts;

   push(@opts, "t", "filter", "$push_type", "$chain");

   unless(exists $option->{"dev"}) {
      my $net_info = network_interfaces();
      my @dev = keys %{$net_info};
      $option->{"dev"} = \@dev;
   }

   if(exists $option->{"dev"} && ! ref($option->{"dev"})) {
      push(@opts, "$dev_type", $option->{"dev"});
   }
   elsif(ref($option->{"dev"}) eq "ARRAY") {
      for my $dev (@{$option->{"dev"}}) {
         my $new_option = $option;
         $new_option->{"dev"} = $dev;

         _open_or_close_port($dev_type, $push_type, $chain, $jump, $port, $new_option);
      }

      return;
   }

   if(exists $option->{"proto"}) {
      push(@opts, "p", $option->{"proto"});
      push(@opts, "m", $option->{"proto"});
   }
   else {
      push(@opts, "p", "tcp");
      push(@opts, "m", "tcp");
   }

   if($port eq "all") {
      push(@opts, "j", "$jump");
   }
   else {
      if(ref($port) eq "ARRAY") {
         for my $port_num (@{$port}) {
            _open_or_close_port($dev_type, $push_type, $chain, $jump, $port_num, $option);
         }
         return;
      }

      push(@opts, "j", $jump);
      push(@opts, "dport", $port);
   }

   iptables @opts;

}

=back

=cut


1;
