The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package IPTables::Mangle;
use strict;
use warnings;

our $VERSION = '0.03';

=head1 NAME

IPTables::Mangle - Manage iptables rules with Perl / YAML

=head1 SYNOPSIS

Given a config file, produces rules for iptables-restore.

Example YAML file, for ease of viewing:

   filter:
       forward: { default: drop }
       foo:
           rules:
              - src: 9.9.9.9
              - src: 10.10.10.10
                action: drop
       input:
           # by default, do not allow any connections unless authorized
           # in the rules below
           default: drop

           # by default, if no "action" is given to a rule below, accept it
           default_rule_action: accept 

           rules:
               # Accept all traffic on loopback interface
               - in-interface: lo

               # Don't disconnect existing connections during a rule change.
               - { match: state, state: 'ESTABLISHED,RELATED' }

               # Allow for pings (no more than 10 a second)
               - { protocol: icmp, icmp-type: 8, match: limit, limit: 10/sec }

               # Allow these IPs, no matter what
               - src: 123.123.123.123

               # example of blocking an IP 
               - { action: drop, src: 8.8.8.8 }

               # example of allowing ip to connect to port 25 (smtp) (one-line)
               - { protocol: tcp, dport: 25, src: 4.2.2.2 }

               # jump to rules defined in "foo" above
               - action: foo

               # if there are no more rules, reject the connection with icmp, don't just let it hang
               - action: reject
                 action_options:
                     reject-with: icmp-admin-prohibited

=head1 DESCRIPTION

This module allows for the management of iptables rules with Perl / YAML.

=head1 TABLES

The top hashref is the table for iptables, this can be either mangle, nat, or filter. 

=head1 CHAINS

The hashref under the top hashref is the chain name.  For system chains the default chainrule can
be set by setting a default hashref in the chain.

$VAR1->{filter}{input} would be the input chain for the filter table.

=head1 CHAIN RULES

Chainrules live in a 'rules' arrayref under the chain, $VAR1->{filter}{input}{rules}, for example.

Every rule in the chain is a hashref which builds a rule.  By default, the jump in the rules, referenced
as 'action' in a rule, is set to accept.  The default action can be modified by changing 
'default_rule_action' in the chain.  Every key in the rule's hashref represents a parameter prefixed by two dashes, '--', 
in an iptables rule.  Two things to note here are that 'action' in a rule really maps to 'jump' in iptables, and 
a special action_options key exists, which references a hashref, which appends options after the iptables jump.  This is 
useful for things like setting '--reject-with' after a jump to reject.


Examples of a chain rule:


# by default, allow this ip

$VAR1->{filter}{input}{rules}[0] = { src => '10.10.10.10' } ;


# allow this ip on port 25 tcp, using accept default

$VAR1->{filter}{input}{rules}[1] = { protocol: 'tcp', dport: 25, src => '10.10.10.10' } ;


# make it explicit

$VAR1->{filter}{input}{rules}[2] = { protocol: 'tcp', dport: 25, src => '10.10.10.10', action => 'accept' } ;


# blacklist an ip

$VAR1->{filter}{input}{rules}[3] = { src => '10.10.10.10', action => 'drop' } ;

# reject with icmp  message

$VAR1->{filter}{input}{rules}[-1] = {
    action => 'reject', 
    action_options => {
        reject-with: 'icmp-admin-prohibited',
    },
};

=cut

=head1 METHODS

=head2 process_config

Given a hashref, produces rules usable by iptables-restore.

Returns one string.

=cut

sub process_config
{
    my $config = shift;

    my $config_out = '';

    $config_out .= _process_table({
        table  => 'mangle',
        chains => [ qw(prerouting input forward output postrouting) ],
        config => $config,
    });

    $config_out .= _process_table({
        table  => 'nat',
        chains => [ qw(prerouting postrouting output) ],
        config => $config,
    });

    $config_out .= _process_table({
        table  => 'filter',
        chains => [ qw(input forward output) ],
        config => $config,
    });

    return $config_out;
}

sub _process_table
{
    my $args = shift;

    my $table  = $args->{table};
    my $config = $args->{config};

    my $table_out = '';
    $table_out .= "*$table\n";

    # configure built-in chains
    $table_out .= ":" . uc($_) . ' ' .
        (exists $config->{$table}{$_} ? 
         uc($config->{$table}{$_}{default}) : 'ACCEPT') . "\n"
            for @{$args->{chains}};

    # configure custom chains
    for my $chain (keys %{$config->{$table} || {} })
    {
        # skip built-in chains
        next if grep { $chain eq $_ } @{$args->{chains}};

        $table_out .= ":" . uc($chain) . " -\n";
    }

    for my $chain (keys %{$config->{$table} || {} })
    {
        $table_out .= &_process_rule({
            target => (
                uc($_->{action} || '') 
             || uc($config->{$table}{$chain}{default_rule_action} || '') 
             || 'ACCEPT'),
            chain  => uc($chain),
            rule   => $_
        }) . "\n" for @{$config->{filter}{$chain}{rules} || []};
    }

    $table_out .= "COMMIT\n";

    return $table_out;
}

sub _process_rule
{
    my $args = shift;
    my $output = "-A $args->{chain} ";

    # remove action from rule
    delete $args->{rule}{action};

    # grab action options
    my $action_opts = delete $args->{rule}{action_options};
    my $action_opts_str = '';

    if ($action_opts)
    {
        $action_opts_str .= "--$_ $action_opts->{$_} "
            for keys %$action_opts;
    }

    $output .= "--$_ $args->{rule}{$_} "
        for keys %{$args->{rule}};

    $output .= "-j $args->{target} $action_opts_str";

    return $output;
}

1;

=head1 COPYRIGHT

Copyright 2011, 2012 Ohio-Pennsylvania Software, LLC.

=head1 LICENSE

This library is free software. You can redistribute it and/or modify it under the same terms as Perl itself.

=head1 AUTHOR

Michael J. Flickinger, C<< <mjflick@gnu.org> >>

=cut