# -*-perl-*-

# $Id: IPTables.plm,v 1.2 2012/10/25 16:34:35 timc Exp $

# What Node to handle IPTables configuration

# $Log: IPTables.plm,v $
# Revision 1.2  2012/10/25 16:34:35  timc
# change in ! behavior for iptables
#
# Revision 1.1  2006/12/19 17:20:32  thomas
# new module
#

=head1 NAME

What::Node::IPTables - Manage iptables firewall rules

=head1 DESCRIPTION

This B<What> configuration package creates iptables firewall rule sets.  The configuration delivered to the target system consists of one or more files in B<iptables-save>/B<iptables-restore> format.

The B</IPTables> schema is as follows:

  /IPTables/ I<instance-name>/ I<table-name>/ I<chain-name>/ I<rule-name>

=over 4

The I<instance-name> allows for the B</IPTables> tree to manage more
than iptables configuration file.  The default instance is named
B<sys>, and is the only one typically configured.  A B<file="...">
attribute controls the output file (e.g. /etc/sysconfig/iptables) for the
B<instance>.  A B<trigger> attached to this node is activated
whenever the target file changes (to reload an rc file, for instance).

The I<table-name> subtree which table (B<filter> or
B<nat>) to which the rule refers.  There are no attributes with semantic
action at this level.

The I<chain-name> subtree identifies the target chain.  A
B<policy="..."> attribute at this level specifies the default policy
for the chain.

The I<rule-name> node creates an iptables rule in the chain, table,
and target file indicated by its location in the configuration tree.
Single-letter attributes will be converted into short-form options in
the resulting rule, and multi-letter attributes will be converted to
long-form options.  Attributes ending in B<_NOT> will have the B<_NOT>
removed, and a B<!> character inserted before the attribute and its
value.  It used to be between but the behavior changed to make rhel6
iptables stop bitching.

The special attribute B<comment="..."> will cause
a comment to be placed immediately above the rule (with the B<#>
character, rule name, and a space automatically added).

Rules are alphanumerically sorted based on the rule name, so by
convention rule names begin with a three-digit numeric code.

=back

=head1 EXAMPLES

Create an empty iptables configuration:

  <node path="/IPTables/sys" file="/etc/sysconfig/iptables" >
    <trigger path="/Init" init="iptables" functions="condrestart" />
  </node>
  <node path="/IPTables/sys/filter/INPUT" policy="ACCEPT" />
  <node path="/IPTables/sys/filter/OUTPUT" policy="ACCEPT" />
  <node path="/IPTables/sys/filter/FORWARD" policy="ACCEPT" />


NAT any packets from the 192.168.8.0/24 network via eth0's ip address:

  <node path="/IPTables/sys/nat/PREROUTING" policy="ACCEPT" />
  <node path="/IPTables/sys/nat/POSTROUTING" policy="ACCEPT" />
  <node path="/IPTables/sys/nat/OUTPUT" policy="ACCEPT" />
  <node path="/IPTables/sys/nat/POSTROUTING/100nat"
        s="192.168.8.0/24" o="eth0" j="MASQUERADE"
        comment="Do the nat" />

Drop outbound nat packets for the lp service:

  <node path="/IPTables/sys/nat/PREROUTING/050printer"
        i="vmnet8" p="tcp" m="tcp" dport="printer" j="DROP"
        comment="(port 515) deny tcp any any eq lpd" />

An equivalent to the last rule, using long options:

  <node path="/IPTables/sys/nat/PREROUTING/050printer"
        in-interface="vmnet8" protocol="tcp" match="tcp"
        dport="printer" jump="DROP"
        comment="(port 515) deny tcp any any eq lpd" />

Use _NOT to negate a test (will result in "--dport ! printer"):

  <node path="/IPTables/sys/nat/PREROUTING/050printer"
        in-interface="vmnet8" protocol="tcp" match="tcp"
        dport_NOT="printer" jump="ACCEPT"
        comment="(port 515) deny tcp any any eq lpd" />

I hope you get the idea...

=head1 GENERATE

This module has a generate method, which takes a file in
B<iptables-save> format and outputs the corresponding XML.

=cut

package What::Node::IPTables;

use strict;
use FileHandle;
use Data::Dumper;

use base qw(What::Node);

our %opts = ();

# Set the priority for this module in $::
$::module_priority{'IPTables'} = 20;

# iptables-restore is very picky about the order of arguments
my @order = (
	     'o',
	     'out-interface',
	     'i',
	     'in-interface',
	     'm',
	     'match',
	     'p',
	     'protocol',
	     's',
	     'source',
	     'd',
	     'destination',
	     'dport',
	     'destination-port',
	     'sport',
	     'source-port',
	    );

my %order = map { $_ => 1 } @order;

###############################################################################
# Apply the current configuration

sub apply {
  my $class = shift;
  my $section = shift;
  my $what = shift;
  my $sname = shift;
  my $cname = shift;

  my ($children, $child);
  my $changes = 0;

  $children = $class->getchildren ($class);
  foreach $child (sort keys %$children) {
    $changes += $children->{$child}->do_instance ($child, $section, $what, $cname);
  }

  return $changes;
}

###############################################################################
# Process a single iptables subtree, replace file and run triggers

sub do_instance {
  my $self = shift;
  my $instance = shift;
  my $section = shift;
  my $what = shift;
  my $cname = shift;

  my ($content, $file, $tables, $table, $chains, $chain, $rules, $rule);
  my ($attr, $attrs, $not);
  $attrs = $self->getattrs ($section);
  if (!$attrs->{file}) {
    $self->err ("No 'file' attribute for /IPTables instance %s\n", $instance);
    return 0;
  }
  $file = (defined ($::opts{root}) ? $::opts{root} : '') . $attrs->{file};

  # Build some content
  $content = <<Eof
#
# This file was created automatically by What::Node::IPTables
# Hand-made changes will be lost!!
#
Eof
  ;
  $tables = $self->getchildren ($section);
  foreach $table (sort keys %$tables) {
    # Table header
    $content .= "\*$table\n";
    $chains = $tables->{$table}->getchildren ($section);
    # Chain headers
    foreach $chain (sort keys %$chains) {
      $attrs = $chains->{$chain}->getattrs ($section);
      $attrs->{policy} = '-' unless $attrs->{policy};
      $content .= ":$chain $attrs->{policy} [0:0]\n";
    }
    # Rules
    foreach $chain (sort keys %$chains) {
      $rules = $chains->{$chain}->getchildren ($section);
      foreach $rule (sort keys %$rules) {
	$attrs = $rules->{$rule}->getattrs ($section);
	# Don't include empty chains
	next unless %$attrs;
	$content .= "# $rule $attrs->{comment}\n" if $attrs->{comment};
	$content .= "-A $chain";
	# iptables-restore requires ordering
	# change the order of the ! to prevent the error:
	    #Using intrapositioned negation (`--option ! this`) is deprecated in favor of extrapositioned (`! --option this`).
	foreach $attr (@order) {
	  if ($attrs->{$attr}) {
	    $content .= (length ($attr)>1?' -':' ') . "-$attr $attrs->{$attr}";
	  } elsif ($attrs->{"${attr}_NOT"}) {
	    $content .= (length ($attr)>1?' -':' ') . "! -$attr " . $attrs->{"${attr}_NOT"};
	  }
	}
	foreach $attr (sort keys %$attrs) {
	  $not = (($attr =~ s/_NOT$//) ? '! ' : '');
	  next if $attr eq "comment";
	  next if $order{$attr};
	  $content .= (length ($attr)>1?' -':' ') . "-$attr $not$attrs->{$attr}";
	}
	# End of rule
	$content .= "\n";
      }
      # End of chain
      $content .= "\n";
    }
    # End of table
    $content .= "\nCOMMIT\n";
  }

  # Now update the system
  if ($self->check_and_replace_content ($file,
					$content,
					0,
					1,
                                        )) {
    $self->run_triggers ($section, $what, $cname);
    return 1;
  }
  return 0;


}

###############################################################################
# Generate some what config from the live system

sub generate {
  my $self = shift;
  my $section = shift;
  my $what = shift;
  my $infile = shift;
  my $instance = shift;

  my ($counter, $data, $fh, @lines, $chain, $policy, $node, $append, @words);
  my ($table, $attr, $attrs, $rule, $counters);

  $infile = "-" unless $infile;
  $instance = "sys" unless $instance;

  #if ($infile eq '-') {
  #  $fh = FileHandle::fdopen (fileno(STDIN), "r");
  #} else {
    $fh = new FileHandle $infile, "r";
  #}
  if (!defined $fh) {
    $self->err ("Failed to run iptables-save!");
    return -1;
  }
  # See Dave parse.  Parse, Dave, parse.
  chomp (@lines = <$fh>);
  while (defined ($_ = shift @lines)) {
    # Wait for table start
    next unless /^\*(\S+)/;
    $table = $1;
    # Slurp chain headings
    while (defined ($_ = shift @lines)) {
      if (/^:(\S+)\s+(ACCEPT|DROP)\s/) {
	($chain, $policy) = ($1, $2);
	$node = $self->find_node ($section, "$instance/$table/$chain", 1);
	$node->setattrs ($section, {policy => $policy});
      } else {
	unshift @lines, $_;
	last;
      }
    }
    # Get rules until a commit
    while (defined ($_ = shift @lines)) {
      last if /^COMMIT$/;
      ($append, $chain, @words) = split;
      next unless $append && $append eq '-A';
      $attrs = {};
      while ($attr = shift @words) {
	if ($attr =~/^(-(\w)|--(\w\w+))$/) {
	  $attrs->{$2 || $3} = shift @words;
	}
      }
      $counters->{$instance}->{$table}->{$chain} = 10
	unless $counters->{$instance}->{$table}->{$chain};
      $rule = sprintf "%03d", $counters->{$instance}->{$table}->{$chain};
      $counters->{$instance}->{$table}->{$chain} += 10;
      $node = $self->find_node ($section, "$instance/$table/$chain/$rule", 1);
      $node->setattrs ($section, $attrs);
    }
  }

  return 0;
}


###############################################################################


1;
