#!/usr/bin/perl
# Coding assistance from Claude Code
#
# 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
##############################################################################

## no critic (ErrorHandling::RequireCarping)
## no critic (CodeLayout::ProhibitParensWithBuiltins)

use 5.014;
use strict;
use warnings;

use English qw(-no_match_vars);
use Fcntl qw(O_CREAT O_TRUNC O_WRONLY);
use File::Basename qw(basename dirname);
use Getopt::Long::Descriptive qw(describe_options);
use IO::Handle;    # enables ->autoflush and ->sync on filehandle globs
use IPC::Run qw(run);

# Standard prefix for iptables rules.
my $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.
my $SUFFIX4 = <<'END_OF_SUFFIX4';
# 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_SUFFIX4

# Standard suffix for ip6tables rules.  IPv6 requires icmp6-port-unreachable
# rather than the IPv4-only icmp-port-unreachable reject type.
my $SUFFIX6 = <<'END_OF_SUFFIX6';
# 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 icmp6-port-unreachable
COMMIT
END_OF_SUFFIX6

# Directory containing iptables rule fragments to combine.
my $FRAGMENT_DIR = '/etc/iptables.d';

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

# Classify a single rule line as IPv4-only, IPv6-only, or protocol-neutral
# based on unambiguous indicators.
#
# The classifier only acts on indicators that are definitively
# protocol-specific: IP literals, the ICMP protocol family
# (-p icmp / -p icmpv6 / -p ipv6-icmp), and protocol-specific
# --reject-with reject types.  A line with no such indicator is
# considered neutral and applies to both protocols.  A line that
# contains both IPv4 and IPv6 indicators simultaneously is genuinely
# contradictory and raises an exception.
#
# Comments and blank lines are not passed here; read_fragment handles
# them separately so they travel with the next rule into the
# appropriate output(s).
#
# $line - One rule line from a fragment file
#
# Returns: 'ipv4', 'ipv6', or 'both'
#  Throws: Text exception if the line contains both IPv4 and IPv6 indicators
sub detect_protocol {
    my ($line) = @_;

    # IPv6 indicators: IPv6 address literal, ICMPv6, or IPv6 reject types.
    # The literal detector requires at least one "::" or two ":"-separated
    # hex groups to avoid false positives on port-only constructs.  Check
    # ICMPv6 BEFORE the IPv4 icmp check below, since "icmpv6" starts with
    # "icmp" and we want the more specific match to win.
    my $is_ipv6
      =  $line =~ m{ :: }xms
      || $line =~ m{ \b [0-9a-fA-F]{1,4} : [0-9a-fA-F]{1,4} \b }xms
      || $line =~ m{ \s -p \s+ icmpv6 \b }xmsi
      || $line =~ m{ \s -p \s+ ipv6-icmp \b }xmsi
      || $line =~ m{ --reject-with \s+ icmp6- }xms;

    # IPv4 indicators: IPv4 address literal, ICMP, or IPv4 reject types.
    # The icmp check uses a negative lookahead to avoid matching icmpv6
    # (the "v6" suffix) -- the regex engine treats the lookahead as zero
    # width so it doesn't consume "v6".
    my $is_ipv4
      =  $line =~ m{ \b \d{1,3} \. \d{1,3} \. \d{1,3} \. \d{1,3} \b }xms
      || $line =~ m{ \s -p \s+ icmp (?!v6) \b }xmsi
      || $line =~ m{ --reject-with \s+ icmp- }xms;

    if ($is_ipv4 && $is_ipv6) {
        chomp(my $shown = $line);
        die "$PROGRAM_NAME: rule has both IPv4 and IPv6 indicators: $shown\n";
    }
    if ($is_ipv4) {
        return 'ipv4';
    }
    if ($is_ipv6) {
        return 'ipv6';
    }
    return 'both';
}

# Read in an iptables fragment and split it into two lists -- lines
# destined for the IPv4 rule set and lines destined for the IPv6 rule
# set -- with rule lines classified by protocol and comment/blank lines
# attached to the next following rule.
#
# Each rule line is passed to detect_protocol and routed to the IPv4
# output, the IPv6 output, or both based on its classification.
#
# Comment and blank lines describe (and visually frame) the rule lines
# that follow them, so they are buffered and emitted along with the
# next rule line into whichever output(s) that rule went to.  This
# avoids leaving orphan comment headers in an output where the rules
# they described were filtered out by classification.  Buffered
# comments at end-of-file with no following rule are silently dropped.
#
# $file - Full path of file to read
#
# Returns: Two array references: (\@ipv4_lines, \@ipv6_lines)
#  Throws: Text exception on failure to read from the file, or if any
#          line is classified as contradictory by detect_protocol
sub read_fragment {
    my ($file) = @_;
    my (@ipv4, @ipv6, @pending);
    open(my $fragment, '<', $file) or die "$PROGRAM_NAME: cannot open $file: $ERRNO\n";
    while (defined(my $line = <$fragment>)) {
        # Historical: very early versions of this setup used a custom
        # "SUL" chain that was later renamed to the standard INPUT
        # chain.  Some deployed fragments still reference "-A SUL", so
        # rewrite those to "-A INPUT" before classification.
        $line =~ s{ \A -A \s+ SUL \s+ }{-A INPUT }xms;

        # Buffer comment and blank lines so they travel with the next
        # rule line into the appropriate output(s).  If the file ends
        # before any non-comment rule, the buffered lines are dropped.
        if ($line =~ m{ \A \s* (?: [#] | \z ) }xms) {
            push(@pending, $line);
            next;
        }

        my $protocol = detect_protocol($line);
        if ($protocol eq 'ipv4' || $protocol eq 'both') {
            push(@ipv4, @pending, $line);
        }
        if ($protocol eq 'ipv6' || $protocol eq 'both') {
            push(@ipv6, @pending, $line);
        }
        @pending = ();
    }
    close($fragment);
    return (\@ipv4, \@ipv6);
}

# Read every fragment in the given directory (skipping dotfiles) in
# sorted order and return two lists of chunks: one for the IPv4 rule
# set and one for the IPv6 rule set.  Each fragment is classified
# per-line by read_fragment, with a blank line appended between
# fragments in each output list.
#
# $dir - Directory to read fragments from
#
# Returns: Two array references: (\@ipv4_body, \@ipv6_body)
#  Throws: Text exception on failure to read from the directory or a file
sub read_all_fragments {
    my ($dir) = @_;
    my (@ipv4_body, @ipv6_body);
    if (-d $dir) {
        opendir(my $fragment_dir, $dir)
          or die "$PROGRAM_NAME: cannot open $dir: $ERRNO\n";
        my @modules = grep { !m{ \A [.] }xms } sort readdir($fragment_dir);
        closedir($fragment_dir);
        for my $name (@modules) {
            my ($v4, $v6) = read_fragment("$dir/$name");
            push(@ipv4_body, @{$v4}, "\n");
            push(@ipv6_body, @{$v6}, "\n");
        }
    }
    return (\@ipv4_body, \@ipv6_body);
}

# Wrap a previously-read fragment body with the standard prefix and the
# given suffix.
#
# $body_ref - Array reference of fragment-body chunks (from read_all_fragments)
# $suffix   - Suffix text to append (e.g., $SUFFIX4 for IPv4, $SUFFIX6 for IPv6)
#
# Returns: Full rule set as a list of chunks
sub build_iptables {
    my ($body_ref, $suffix) = @_;
    return ($PREFIX, "\n", @{$body_ref}, $suffix);
}

# 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) = @_;

    # Create the temp file with mode 0600 regardless of the process umask.
    # iptables rules describe the system's security posture and should not
    # be world-readable.
    sysopen(my $new, "${file}.new", O_WRONLY | O_CREAT | O_TRUNC, 0600)
      or die "$PROGRAM_NAME: cannot create ${file}.new: $ERRNO\n";
    print {$new} @data
      or die "$PROGRAM_NAME: cannot write to ${file}.new: $ERRNO\n";

    # fsync the file data to disk before the rename, so that a crash
    # between rename and the kernel's eventual flush cannot leave a
    # zero-byte or partially-written rules file in place.
    $new->sync
      or die "$PROGRAM_NAME: cannot fsync ${file}.new: $ERRNO\n";
    close($new)
      or die "$PROGRAM_NAME: cannot flush ${file}.new: $ERRNO\n";
    rename("$file.new", $file)
      or die "$PROGRAM_NAME: cannot install new $file: $ERRNO\n";

    # fsync the containing directory so the rename itself survives a
    # crash; without this the directory entry change may still be lost
    # even though the file data was flushed above.
    my $dir = dirname($file);
    open(my $dfd, '<', $dir)
      or die "$PROGRAM_NAME: cannot open $dir: $ERRNO\n";
    $dfd->sync
      or die "$PROGRAM_NAME: cannot fsync $dir: $ERRNO\n";
    close($dfd);
    return;
}

# Run an external command via IPC::Run, optionally feeding it data on
# stdin, and return its captured stdout, stderr, and exit code.  This
# function does not die; the caller is expected to inspect the exit code
# and act on it.  If the command cannot be started at all (e.g., binary
# missing or not executable), an exit code of -1 is returned and the
# IPC::Run failure message is placed in stderr so that callers see a
# uniform "command failed" shape regardless of which kind of failure
# occurred.
#
# $cmd_ref - Array reference of the command and its arguments
# $stdin   - Optional string to feed to the command's standard input
#
# Returns: List of (stdout, stderr, exitcode)
sub run_command {
    my ($cmd_ref, $stdin) = @_;
    my $in = defined($stdin) ? $stdin : q{};
    my ($out, $err) = (q{}, q{});
    my $ok = eval {
        run($cmd_ref, \$in, \$out, \$err);
        1;
    };
    if (!$ok) {
        my $why = $EVAL_ERROR;
        chomp $why;
        return ($out, "cannot run $cmd_ref->[0]: $why", -1);
    }
    my $exitcode = $CHILD_ERROR >> 8;
    return ($out, $err, $exitcode);
}

# Die with a useful message if a run_command result indicates failure.
# The message includes both stderr and stdout, since different commands
# (and different versions of the same command) split diagnostic detail
# between the two streams.
#
# $context - Short description for the error message; phrased so it
#            reads naturally after "cannot " (e.g. "reload iptables")
# $out     - Captured stdout from run_command
# $err     - Captured stderr from run_command
# $rc      - Exit code from run_command
#
# Returns: undef on success
#  Throws: Text exception if $rc is non-zero
sub die_on_failure {
    my ($context, $out, $err, $rc) = @_;
    if ($rc == 0) {
        return;
    }
    my $detail = $err;
    if (length($out) > 0) {
        $detail .= $out;
    }
    die "$PROGRAM_NAME: cannot $context (rc=$rc): $detail";
}

# Run a generated rule set through "iptables-restore --test" (or the IPv6
# equivalent) to validate it without touching the kernel or disk.  If the
# rules are malformed this dies before any side effect, leaving the on-disk
# file and the running kernel rules consistent with each other.
#
# $cmd   - Path to iptables-restore or ip6tables-restore
# $rules - Array reference to the generated rule set
#
# Returns: undef
#  Throws: Text exception if the rules fail validation
sub test_rules {
    my ($cmd, $rules) = @_;
    my $input = join(q{}, @{$rules});
    my ($out, $err, $rc) = run_command([$cmd, '--test'], $input);
    die_on_failure("validate rules with $cmd", $out, $err, $rc);
    return;
}

# Given arrays of new iptables data for IPv4 and IPv6, install new iptables
# configurations and load them into the kernel.  The exact mechanism and
# paths vary by operating system:
#
#   Debian/Ubuntu:
#     IPv4 -> /etc/iptables/general       (reloaded via iptables-restore)
#     IPv6 -> /etc/iptables/general6      (reloaded via ip6tables-restore)
#
#   Red Hat:
#     IPv4 -> /etc/sysconfig/iptables     (reloaded via "systemctl restart iptables")
#     IPv6 -> /etc/sysconfig/ip6tables    (reloaded via "systemctl restart ip6tables")
#
# $rules4 - Array reference to new IPv4 iptables data
# $rules6 - Array reference to new IPv6 ip6tables 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 ($rules4, $rules6) = @_;

    # 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 "$PROGRAM_NAME: cannot mkdir /etc/iptables: $ERRNO\n";
        }
        # Write both files before reloading either, so a write failure
        # (e.g., disk full) leaves the kernel rules and on-disk rules
        # consistent with each other (both still the old version).
        write_file('/etc/iptables/general',  @{$rules4});
        write_file('/etc/iptables/general6', @{$rules6});
        my ($out, $err, $rc)
          = run_command(['/usr/sbin/iptables-restore', '/etc/iptables/general']);
        die_on_failure('reload iptables', $out, $err, $rc);
        ($out, $err, $rc)
          = run_command(['/usr/sbin/ip6tables-restore', '/etc/iptables/general6']);
        die_on_failure('reload ip6tables', $out, $err, $rc);
    } elsif (-f '/etc/redhat-release') {
        # Write both files before restarting either service; see Debian
        # branch above for rationale.
        write_file('/etc/sysconfig/iptables',  @{$rules4});
        write_file('/etc/sysconfig/ip6tables', @{$rules6});
        my ($out, $err, $rc)
          = run_command(['/usr/bin/systemctl', 'restart', 'iptables']);
        die_on_failure('reload iptables', $out, $err, $rc);
        ($out, $err, $rc)
          = run_command(['/usr/bin/systemctl', 'restart', 'ip6tables']);
        die_on_failure('reload ip6tables', $out, $err, $rc);
    } else {
        die "$PROGRAM_NAME: 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 = $PROGRAM_NAME;
$PROGRAM_NAME = basename($PROGRAM_NAME);

# 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 IPv4 and IPv6 rules without updating them'],
    ['print4',         'Print out the generated IPv4 rules only (no headers; byte-identical to the installed file)'],
    ['print6',         'Print out the generated IPv6 rules only (no headers; byte-identical to the installed file)'],
    ['fragment-dir=s', "Read fragments from this directory instead of the default ($FRAGMENT_DIR)"],
    ['no-validate',    'Skip the iptables-restore --test pre-flight validation (intended for tests; not recommended for production use)'],
);

# The print options are mutually exclusive.
my $print_count = 0;
for my $opt_name (qw(print print4 print6)) {
    if ($opt->$opt_name) {
        $print_count++;
    }
}
if ($print_count > 1) {
    die "$PROGRAM_NAME: --print, --print4, and --print6 are mutually exclusive\n";
}
if ($opt->help) {
    print $usage->text
      or die "$PROGRAM_NAME: cannot write to standard output: $ERRNO\n";
    exit(0);
} elsif ($opt->manual) {
    print "Feeding myself to perldoc, please wait...\n"
      or die "$PROGRAM_NAME: cannot write to standard output: $ERRNO\n";
    exec('perldoc', '-t', $fullpath)
      or die "$PROGRAM_NAME: cannot exec perldoc: $ERRNO\n";
}

# Determine which fragment directory to read from.  If the user passed
# --fragment-dir, require that directory to exist (the default
# directory is allowed to be missing, in which case the rule set just
# has no fragment body).
my $fragment_dir = $opt->fragment_dir // $FRAGMENT_DIR;
if (defined($opt->fragment_dir) && !-d $fragment_dir) {
    die "$PROGRAM_NAME: fragment directory does not exist: $fragment_dir\n";
}

# Build the iptables rules for this host (IPv4 and IPv6).  Fragment
# lines are classified per-protocol so IPv4-only and IPv6-only rules
# end up in the correct rule set; protocol-neutral lines (including
# comments and blank lines) appear in both.
my ($v4_body, $v6_body) = read_all_fragments($fragment_dir);
my @rules4 = build_iptables($v4_body, $SUFFIX4);
my @rules6 = build_iptables($v6_body, $SUFFIX6);

# Validate both rule sets before either printing or installing, so --print
# accurately previews what would be installed.  If either set is malformed
# this dies before any side effect.  The validation can be skipped via
# --no-validate (intended for test environments that lack iptables-restore
# or cannot run it as root).
if (!$opt->no_validate) {
    test_rules('/usr/sbin/iptables-restore',  \@rules4);
    test_rules('/usr/sbin/ip6tables-restore', \@rules6);
}

# If told to just print out the results, do so.  Otherwise, install the new
# rules.  --print4 and --print6 emit one rule set with no header so the
# output is byte-identical to what would be written to disk (useful for
# capturing reference fixtures for tests).  --print keeps the header form
# for backward compatibility.
if ($opt->print) {
    print "# IPv4 rules\n", @rules4, "\n# IPv6 rules\n", @rules6
      or die "$PROGRAM_NAME: cannot write to standard output: $ERRNO\n";
} elsif ($opt->print4) {
    print @rules4
      or die "$PROGRAM_NAME: cannot write to standard output: $ERRNO\n";
} elsif ($opt->print6) {
    print @rules6
      or die "$PROGRAM_NAME: cannot write to standard output: $ERRNO\n";
} else {
    install_iptables(\@rules4, \@rules6);
}
exit(0);
__END__

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

=for stopwords
Digant Kasundra iptables ip6tables rebuild-iptables Allbery Lewenberg TCP
UDP ICMP ICMPv6 IPv4 IPv6 ifup loopback startup fsync

=head1 NAME

rebuild-iptables - Install an iptables rules file from fragments

=head1 SYNOPSIS

rebuild-iptables [B<-h>] [B<-m>] [B<-p>]

rebuild-iptables [B<--help>] [B<--manual>] [B<--print>] [B<--print4>] [B<--print6>]
[B<--fragment-dir>=I<DIR>] [B<--no-validate>]

=head1 DESCRIPTION

B<rebuild-iptables> constructs iptables configuration files by
concatenating various modules found in F</etc/iptables.d>.  Separate
IPv4 and IPv6 rule sets are generated from the same fragments (with
different reject suffixes appropriate to each protocol), written to the
appropriate files for either Red Hat or Debian (determined automatically),
and loaded into the kernel via B<iptables-restore> and B<ip6tables-restore>.

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<#>).  Comment and blank lines are buffered and emitted
along with the next rule line into whichever output the rule went to,
so a comment header above a block of rules follows those rules into
the appropriate output and does not appear orphaned in the other.
Trailing comments at end-of-file with no following rule are silently
dropped.

Each rule line is classified as IPv4-only, IPv6-only, or
protocol-neutral based on unambiguous indicators: IPv4 or IPv6 address
literals, the ICMP protocol family (C<-p icmp> versus C<-p icmpv6> or
C<-p ipv6-icmp>), and protocol-specific C<--reject-with> reject types.
A line with no such indicator is considered neutral and applies to
both protocols.  A line that contains both IPv4 and IPv6 indicators
simultaneously causes B<rebuild-iptables> to exit with an error.

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 (using the IPv4 or IPv6 reject
type as appropriate for each rule set).

=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.  The output begins with a C<# IPv4 rules> header line,
followed by the IPv4 rule set, then a C<# IPv6 rules> header line and
the IPv6 rule set.

=item B<--print4>

Like B<--print> but emits only the IPv4 rule set, with no header line.
The output is byte-identical to what would be written to the IPv4 rules
file on disk, which makes it suitable for capturing reference fixtures
to test against.  Mutually exclusive with B<--print> and B<--print6>.

=item B<--print6>

Like B<--print> but emits only the IPv6 rule set, with no header line.
The output is byte-identical to what would be written to the IPv6 rules
file on disk, which makes it suitable for capturing reference fixtures
to test against.  Mutually exclusive with B<--print> and B<--print4>.

=item B<--fragment-dir>=I<DIR>

Read fragments from I<DIR> instead of the default F</etc/iptables.d>.
If I<DIR> does not exist, B<rebuild-iptables> exits with an error.
The output paths for installed rules files are unaffected; this option
only changes where fragments are read from.  Useful for running the
script against a test fixture directory.

=item B<--no-validate>

Skip the C<iptables-restore --test> and C<ip6tables-restore --test>
pre-flight validation.  Intended for use by the test suite (where
running the validator may require root or may not be available at all);
not recommended for production use, since it removes the safety net
that catches malformed rules before any file is written or kernel
rules are reloaded.

=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 IPv4 configuration file on Debian.
The F</etc/iptables> directory will be created if it doesn't exist.

=item F</etc/iptables/general6>

The install location of the generated IPv6 configuration file on Debian.

=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 IPv4 configuration file on Red Hat.

=item F</etc/sysconfig/ip6tables>

The install location of the generated IPv6 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> (and F</etc/iptables/general6> for IPv6) before
bringing up a network interface.

Before any file is written, any kernel rules are reloaded, or any output
is printed, both the IPv4 and IPv6 generated rule sets are validated by
piping them through C<iptables-restore --test> and C<ip6tables-restore
--test> respectively.  If either set is malformed, B<rebuild-iptables>
exits with an error before touching any file, so the on-disk rules files
and the running kernel rules remain consistent with each other on
failure.  This validation also runs when B<--print>, B<--print4>, or
B<--print6> is given, so the printed output is an accurate preview of
what would be installed.  The validation can be bypassed with
B<--no-validate>, which is intended for test environments that lack
B<iptables-restore> or cannot run it as root; bypassing the validation
is not recommended for production use.

The generated rules files are written atomically (via a temporary
F<.new> file and B<rename>) with mode 0600, and the file data and
containing directory are both B<fsync>'d so that the new rules survive
a system crash that occurs during installation.

=head1 AUTHOR

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

Updated in 2026 by Adam H. Lewenberg <adamhl@stanford.edu> and Claude Code.

=head1 SEE ALSO

iptables(8)

=cut
