#!/usr/bin/perl
#
# Construct an iptables rules file from fragments.
#
# Given a directory full of iptables configuration fragments, this script adds
# a standard prefix and suffix to build a complete set of iptables rules and
# then loads it into the kernel.
#
# Written by Russ Allbery <rra@stanford.edu>
# Adapted by Digant C Kasundra <digant@stanford.edu>
# Copyright 2005, 2006, 2013
#     The Board of Trustees of the Leland Stanford Junior University
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

##############################################################################
# Modules and declarations
##############################################################################

use 5.008;
use strict;
use warnings;

use File::Basename qw(basename);
use Getopt::Long::Descriptive qw(describe_options);
use IO::Handle;

# Standard prefix for iptables rules.
our $PREFIX = <<'END_OF_PREFIX';
*filter
:INPUT ACCEPT
:FORWARD ACCEPT
:OUTPUT ACCEPT
-A INPUT -i lo -j ACCEPT
END_OF_PREFIX

# Standard suffix for iptables rules.
our $SUFFIX = <<'END_OF_SUFFIX';
# Rejects all remaining connections with port-unreachable errors.
-A INPUT -p tcp --syn -j REJECT --reject-with tcp-reset
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
COMMIT
END_OF_SUFFIX

##############################################################################
# iptables manipulation
##############################################################################

# Read in an iptables fragment.  This is almost just a slurp of the file, but
# we rewrite the SUL chain (which we used to use in the very beginnings of
# iptables) to the standard INPUT chain.
#
# $file - Full path of file to read
#
# Returns: Text of the file as an array of lines or a string
#  Throws: Text exception on failure to read from the file
sub read_fragment {
    my ($file) = @_;
    my @data;
    open(my $fragment, '<', $file) or die "$0: cannot open $file: $!\n";
    while (!eof($fragment)) {
        my $line;
        if (!defined($line = <$fragment>)) {
            die "$0: cannot read from $file: $!\n";
        }
        $line =~ s{ \A -A \s+ SUL \s+ }{-A INPUT }xms;
        push(@data, $line);
    }
    close($fragment) or die "$0: cannot close $file: $!\n";
    return wantarray ? @data : join(q{}, @data);
}

# Build the iptables rules for this system.  Reads fragments from the
# /etc/iptables.d directory, prepends the prefix and appends the suffix, and
# returns the result as either an array of lines or a string.
#
# Returns: Text of the file as an array of lines or a string
#  Throws: Text exception on failure to read from the file
sub build_iptables {
    my @rules;

    # Add the prefix and a blank line.
    push(@rules, $PREFIX, "\n");

    # Build the list of fragment paths, which is every file in /etc/iptables.d
    # that does not begin with a period.
    my @fragments;
    if (-d '/etc/iptables.d') {
        opendir(my $fragment_dir, '/etc/iptables.d')
          or die "$0: cannot open /etc/iptables.d: $!\n";
        my @modules = grep { !m{ \A [.] }xms } sort readdir($fragment_dir);
        closedir($fragment_dir)
          or die "$0: cannot read from /etc/iptables.d: $!\n";
        @fragments = map { '/etc/iptables.d/' . $_ } @modules;
    }

    # Append the fragment contents with a blank line between them.
    for my $fragment (@fragments) {
        push(@rules, read_fragment($fragment), "\n");
    }

    # Append the suffix and return the results.
    push(@rules, $SUFFIX);
    return wantarray ? @rules : join(q{}, @rules);
}

# Write lines to a file atomically, using a separate file and then atomically
# replacing the file.  Uses $file with ".new" appended as the temporary file.
#
# $file - Output file name
# @data - List of chunks of data to put into the file
#
# Returns: undef
#  Throws: Text exception on failure to write to or rename the file
sub write_file {
    my ($file, @data) = @_;
    open(my $new, '>', "${file}.new")
      or die "$0: cannot create ${file}.new: $!\n";
    print {$new} @data
      or die "$0: cannot write to ${file}.new: $!\n";
    close($new)
      or die "$0: cannot flush ${file}.new: $!\n";
    rename("$file.new", $file)
      or die "$0: cannot install new $file: $!\n";
    return;
}

# Given the array of new iptables data, install a new iptables configuration
# and load it into the kernel.  The exact mechanism and paths vary by
# operating system.
#
# @rules - New iptables data
#
# Returns: undef
#  Throws: Text exception on failure to write new data or reload it
#          Text exception on failure to detect the operating system
sub install_iptables {
    my @rules = @_;

    # Decide what to do on the basis of file existence.  Treat Ubuntu the same
    # as Debian for our purposes.
    if (-f '/etc/debian_version') {
        if (!-d '/etc/iptables') {
            mkdir('/etc/iptables', 0755)
              or die "$0: cannot mkdir /etc/iptables: $!\n";
        }
        write_file('/etc/iptables/general', @rules);
        system('/sbin/iptables-restore < /etc/iptables/general') == 0
          or die "$0: cannot reload iptables\n";
    } elsif (-f '/etc/redhat-release') {
        write_file('/etc/sysconfig/iptables', @rules);
        system('/sbin/service', 'iptables', 'restart') == 0
          or die "$0: cannot reload iptables\n";
    } else {
        die "$0: cannot detect OS type or OS not supported\n";
    }
    return;
}

##############################################################################
# Main routine
##############################################################################

# Always flush output.
STDOUT->autoflush;

# Clean up the script name for error reporting.
my $fullpath = $0;
local $0 = basename($0);

# Parse the argument list.
my ($opt, $usage) = describe_options(
    '%c %o',
    ['help|h',       'Print usage message and exit'],
    ['manual|man|m', 'Print full manual and exit'],
    ['print|p',      'Print out the generated rules without updating them'],
);
if ($opt->help) {
    print {*STDOUT} $usage->text
      or die "$0: cannot write to standard output: $!\n";
    exit(0);
} elsif ($opt->manual) {
    print {*STDOUT} "Feeding myself to perldoc, please wait...\n"
      or die "$0: cannot write to standard output: $!\n";
    exec('perldoc', '-t', $fullpath);
}

# Build the iptables rules for this host.
my @rules = build_iptables();

# If told to just print out the results, do so.  Otherwise, install the new
# rules.
if ($opt->print) {
    print {*STDOUT} @rules
      or die "$0: cannot write to standard output: $!\n";
} else {
    install_iptables(@rules);
}
exit(0);
__END__

##############################################################################
# Documentation
##############################################################################

=for stopwords
Digant Kasundra iptables rebuild-iptables Allbery TCP UDP -hmp ifup loopback
startup

=head1 NAME

rebuild-iptables - Install an iptables rules file from fragments

=head1 SYNOPSIS

rebuild-iptables [B<-hmp>]

=head1 DESCRIPTION

B<rebuild-iptables> constructs an iptables configuration file by
concatenating various modules found in F</etc/iptables.d>.  The resulting
iptables configuration file is written to the appropriate file for either
Red Hat or Debian (determined automatically) and loaded into the kernel.

Each module is just a text file located in the directory mentioned above
that contains one or more iptables configuration lines (basically the
arguments to an B<iptables> invocation), blank lines, or comments (lines
starting with C<#>).  Comments and blank lines will be ignored by the
system.

Along with the modules in the directory specified, a standard prefix and
suffix will be added automatically.  The prefix sets up default ACCEPT
behaviors for OUTPUT and FORWARD, and automatically accepts all loopback
traffic.  The suffix rejects all unaccepted traffic to the INPUT chain
with appropriate errors for TCP and UDP.

=head1 OPTIONS

=over 4

=item B<-h>, B<--help>

Print a short usage message and exit.

=item B<-m>, B<--manual>, B<--man>

Display this manual and exit.

=item B<-p>, B<--print>

Rather than installing the new rules and loading them into the kernel,
just print the combined rules to standard output and exit.  This will
include all of the comments and blank lines that would be in the rule set
as stored on disk.

=back

=head1 FILES

=over 4

=item F</etc/debian_version>

If this file exists, the system is assumed to be a Debian system for
determining the installation location and actions to load the new rules
into the kernel.

=item F</etc/iptables.d>

Every file in this directory that does not start with C<.> is assumed to
be a set of iptables rules, and its contents are added to the generated
rule set.

=item F</etc/iptables/general>

The install location of the generated configuration file on Debian.  The
F</etc/iptables> directory will be created if it doesn't exist.

=item F</etc/redhat-release>

If this file exists and F</etc/debian_version> does not, the system is
assumed to be a Red Hat system for determining the installation location
and actions to load the new rules into the kernel.

=item F</etc/sysconfig/iptables>

The install location of the generated configuration file on Red Hat.

=back

=head1 NOTES

On Red Hat, there is an existing startup script that loads iptables rules
from F</etc/sysconfig/iptables> into the kernel during boot, so nothing
is needed besides this script.  On Debian, however, there is no standard
startup script that does this, and B<rebuild-iptables> only loads the rules
into the kernel when run.  Standard practice when using this script is to
add an B<ifup> hook in F</etc/network/if-pre-up.d> to load the rules from
F</etc/iptables/general> before bringing up a network interface.

=head1 AUTHOR

Russ Allbery <rra@stanford.edu> and
Digant C Kasundra <digant@stanford.edu>.

=head1 SEE ALSO

iptables(8)

=cut
