#!/usr/bin/perl -w
#
###############################################################################
#
# File: fwsnort
#
# Purpose: To translate snort rules into equivalent iptables rules.
#          fwsnort is based on the original snort2iptables shell script
#          written by William Stearns.
#
# Author: Michael Rash <mbr@cipherdyne.org>
#
# Credits: (see the CREDITS file)
#
# Version: 0.7.0
#
# Copyright (C) 2003-2005 Michael Rash (mbr@cipherdyne.org)
#
# License (GNU Public License):
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
#    USA
#
# TODO:
#   - Add the ability to remove rules from a real snort config in the same
#     way we remove them from iptables rulesets in fwsnort (we remove rules
#     from an iptables ruleset if the iptables policy will not allow such
#     traffic through in the first place).
#   - Implement code for the various SERVER variables.
#   - fwsnort.sh generation timestamp.
#   - Two new options: --ipt-tos and --ipt-mark.
#
# Snort Rule Options:
#
#   msg:           Prints a message in alerts and packet logs.
#   logto:         Log the packet to a user specified filename instead of the
#                  standard output file.
#   ttl:           Test the IP header's TTL field value.
#   tos:           Test the IP header's TOS field value.
#   id:            Test the IP header's fragment ID field for a specific
#                  value.
#   ipoption:      Watch the IP option fields for specific codes.
#   fragbits:      Test the fragmentation bits of the IP header.
#   dsize:         Test the packet's payload size against a value.
#   flags          Test the TCP flags for certain values.
#   seq:           Test the TCP sequence number field for a specific value.
#   ack:           Test the TCP acknowledgement field for a specific value.
#   itype:         Test the ICMP type field against a specific value.
#   icode:         Test the ICMP code field against a specific value.
#   icmp_id:       Test the ICMP ECHO ID field against a specific value.
#   icmp_seq:      Test the ICMP ECHO sequence number against a specific
#                  value.
#   content:       Search for a pattern in the packet's payload.
#   content-list:  Search for a set of patterns in the packet's payload.
#   offset:        Modifier for the content option, sets the offset to begin
#                  attempting a pattern match.
#   depth:         Modifier for the content option, sets the maximum search
#                  depth for a pattern match attempt.
#   nocase:        Match the preceding content string with case insensitivity.
#   session        Dumps the application layer information for a given
#                  session.
#   rpc:           Watch RPC services for specific application/procedure
#                  calls.
#   resp:          Active response (knock down connections, etc).
#   react:         Active response (block web sites).
#   reference:     External attack reference ids.
#   sid:           snort rule id.
#   rev:           Rule revision number.
#   classtype:     Rule classification identifier.
#   priority:      Rule severity identifier.
#   uricontent:    Search for a pattern in the URI portion of a packet
#
#   tag:           Advanced logging actions for rules.
#   ip_proto:      IP header's protocol value.
#   sameip:        Determines if source ip equals the destination ip.
#   stateless:     Valid regardless of stream state.
#   regex:         Wildcard pattern matching.
#
############################################################################
#
# $Id: fwsnort,v 1.94 2005/06/06 03:10:01 mbr Exp $
#

use lib '/usr/lib/fwsnort';
use IO::Socket;
use IPTables::Parse;
use Net::IPv4Addr qw(ipv4_network);
use File::Copy;
use File::Path;
use Sys::Hostname;
use Getopt::Long;
use strict;

#======================== config ========================
my $fwsnort_dir = '/etc/fwsnort';
my $rules_dir   = "${fwsnort_dir}/snort_rules";
my $archive_dir = "${fwsnort_dir}/archive";
my $log_dir     = '/var/log';

### Snort.org no longer allows auto downloads of signatures
my $bleeding_snort_website = 'www.bleedingsnort.com';

### config file
my $fwsnort_conf = "${fwsnort_dir}/fwsnort.conf";

### log file
my $logfile = "${log_dir}/fwsnort.log";

### iptables script
my $ipt_script = "${fwsnort_dir}/fwsnort.sh";
#===================== end config =======================

### version number
my $version = '0.7.0';

### supported variables in snort signatures
my %snort_vars = (
    'EXTERNAL_NET'    => '',
    'HOME_NET'        => '',
    'HTTP_SERVERS'    => '',
    'SMTP_SERVERS'    => '',
    'DNS_SERVERS'     => '',
    'HTTP_PORTS'      => '',
    'TELNET_SERVERS'  => '',
    'AIM_SERVERS'     => '',
    'SQL_SERVERS'     => '',
    'SHELLCODE_PORTS' => '',
    'ORACLE_PORTS'    => ''
);

my %iptables_opts = (
    'uricontent' => '-m string',
    'content'  => '-m string',
    'ipopts'   => '-m ipv4options',
    'flags'    => '--tcp-flags',
    'flow'     => '--tcp-flags',  ### revert to keeping track of ACK
                                  ### flags (usually ok).
    'itype'    => '--icmp-type',
    'ttl'      => '-m ttl',
    'tos'      => '--tos',
    'seq'      => '--log-tcp-sequence',
    'src'      => '-s',
    'src_p'    => '--sport',
    'dst'      => '-d',
    'dst_p'    => '--dport',
    'proto'    => '-p',
    'ip_proto' => '-p',
    'resp'     => '-j REJECT'
);

my %snort_opts = (
    ### snort options that we can directly filter on
    ### in iptables rulesets (snort options are separate
    ### from the snort "header" which include protocol,
    ### source, destination, etc.)
    'filter' => {
        'uricontent' => '[\s;]uricontent:\s*\"(.*?)\"\s*;',
        'content'  => '[\s;]content:\s*\"(.*?)\"\s*;',
        'flags'    => '[\s;]flags:\s*(.*?)\s*;',
        'itype'    => '[\s;]itype:\s*(\d+)\s*;',
        'ttl'      => '[\s;]ttl:\s*(.*?)\s*;',
        'tos'      => '[\s;]tos:\s*(\d+)\s*;',
        'ipopts'   => '[\s;]ipopts:\s*(rr|ssrr|lsrr|lsrre)\s*;',
        'flow'     => '[\s;]flow:\s*(.*?)\s*;',
        'ip_proto' => '[\s;]ip_proto:\s*(.*?)\s*;',
        'resp'     => '[\s;]resp:\s*(.*?)\s*;'
    },

    ### snort options that can be put into iptables
    ### ruleset, but only in log messages with --log-prefix
    'logprefix' =>  {
        'sid'       => '[\s;]sid:\s*(\d+)\s*;',
        'msg'       => 'msg:\s*\"(.*?)\"\s*;',
        'classtype' => '[\s;]classtype:\s*(.*?)\s*;',
        'reference' => '[\s;]reference:\s*(.*?)\s*;',
        'priority'  => '[\s;]priority:\s*(\d+)\s*;',
        'rev'       => '[\s;]rev:\s*(\d+)\s*;',
    },

    ### snort options that cannot be included directly
    ### within iptables filter statements (yet :)
    'unsupported' => {
        'pcre'         => '[\s;]pcre:\s*.*?\s*;',
        'content-list' => '[\s;]content-list:\s*.*?\s*;',
        'dsize'        => '[\s;]dsize:\s*\S+\s*;',
        'ack'          => '[\s;]ack:\s*\d+\s*;',
        'fragbits'     => '[\s;]fragbits:\s*\S+\s*;',
        'content-list' => '[\s;]content\-list:\s*\".*?\"\s*;',
        'rpc'          => '[\s;]rpc:\s*\S+\s*;',
        'byte_test'    => '[\s;]byte_test\s*.*?\s*;',
        'byte_jump'    => '[\s;]byte_jump\s*.*?\s*;',
        'distance'     => '[\s;]distance:\s*(\d+)\s*;',
        'within'       => '[\s;]within:\s*(\d+)\s*;',
        'flowbits'     => '[\s;]flowbits:\s*\S+\s*;',
#        'offset'       => '[\s;]offset:\s*\d+\s*;',
#        'depth'        => '[\s;]depth:\s*\d+\s*;',
#        'ipopts'  => '[\s;]ipopts:\s*(\w+)\s*;',

        ### the following fields get logged by iptables but
        ### we cannot filter them directly (yet).  Functionality
        ### is being built into psad to generate alerts based
        ### on these Snort options.
        'id'       => '[\s;]id:\s*(\d+)\s*;',
        'seq'      => '[\s;]seq:\s*(\d+)\s*;',  ### --log-tcp-sequence
        'icmp_seq' => '[\s;]icmp_seq:\s*(\d+)\s*;',
        'icmp_id'  => '[\s;]icmp_id:\s*(\d+)\s*;',
        'icode'    => '[\s;]icode:\s*(\d+)\s*;',
        'sameip'   => '[\s;]sameip\s*;',
        'regex'    => '[\s;]regex:\s*.*?\s*;',
    },

    ### snort options that fwsnort will ignore
    'ignore' => {
        'offset'  => '[\s;]offset:\s*\d+\s*;',
        'depth'   => '[\s;]depth:\s*\d+\s*;',
        'nocase'  => '[\s;]nocase\s*;',
        'logto'   => '[\s;]logto:\s*\S+\s*;',
        'session' => '[\s;]session\s*;',
        'tag'     => '[\s;]tag:\s*.*?\s*;',
        'react'   => '[\s;]react:\s*.*?\s*;'  ### psad can react
    }
);

### array that contains iptables script (will be written
### to $ipt_script)
my @ipt_script_lines = ();

### variables that are supported by the fwsnort config
my %conf_supported_vars = ();

### port config (see any snort.conf file)
my @http_ports      = ();
my @shellcode_ports = ();
my @oracle_ports    = ();

### interface config
my %intf_net  = ();  ### corresponds to $EXTERNAL_NET and $HOME_NET
my %intf_type = ();

### server config
my @http_servers   = ();
my @smtp_servers   = ();
my @dns_servers    = ();
my @sql_servers    = ();
my @telnet_servers = ();
my @aim_servers    = ();

### contains the names of user-defined iptables chains
### for ### fwsnort rules
my %fwsnort_chains;

### contains a cache of the iptables policy
my %ipt_policy;

### For each internal interface fwsnort will create two associated iptables
### chains.  For example, suppose eth0 is the external interface and
### eth1 is the internal interface:
###   jump int: eth0   dst: internal network  (src doesn't matter)
###   jump int: eth1   src: internal network  (dst doesn't matter)

### regex to match ip addresses
my $ip_re = '(?:\d{1,3}\.){3}\d{1,3}';

### arrays of ips or networks to ignore (i.e. LOG, DROP, and REJECT
### rules will never be applied).
my @ignore_ips  = ();
my @ignore_nets = ();

### config and commands hashes (constructed by readconf())
my %config;
my %cmds;

### establish some default behavior
my $ipt_apply  = 0;
my $ipt_drop   = 0;
my $ipt_reject = 0;
my $help       = 0;
my $stdout     = 0;
my $debug      = 0;
my $strict     = 0;
my $snort_sid  = 0;
my $dump_conf  = 0;
my $verbose    = 0;
my $print_ver  = 0;
my $update_rules   = 0;  ### used to download latest snort rules
my $ipt_print_type = 0;
my $ipt_rule_ctr   = 1;
my $ipt_sync       = 1;
my $no_ipt_sync    = 0;
my $no_ipt_log     = 0;
my $no_ipt_test    = 0;
my $no_ipt_jumps   = 0;
my $add_deleted    = 0;
my $rules_type     = '';
my $snort_type     = '';
my $int_net        = '';
my $dmz_net        = '';
my $ulog_nlgroup   = 1;
my $ulog_mode      = 0;

### make Getopts case sensitive
Getopt::Long::Configure('no_ignore_case');

&usage(1) unless (GetOptions(
    'ipt-apply'      => \$ipt_apply,    # Apply the generated ruleset.
    'ipt-drop'       => \$ipt_drop,     # Add iptables DROP rules.
    'ipt-reject'     => \$ipt_reject,   # Add iptables REJECT rules.
    'ipt-script=s'   => \$ipt_script,   # Manually specify the path to the
                                        # generated iptables script.
    'internal-net=s' => \$int_net,      # Manually specify internal net.
    'dmz-net=s'      => \$dmz_net,      # Manually specify dmz net.
    'snort-sid=i'    => \$snort_sid,    # Parse only this particular snort rule.
    'type=s'         => \$rules_type,   # Process this type of snort rule
                                        # (e.g. "ddos")
    'snort-rdir=s'   => \$rules_dir,    # Manually specify the snort rules
                                        # directory.
    'no-ipt-sync'    => \$no_ipt_sync,  # Do not sync with the iptables policy.
    'no-ipt-log'     => \$no_ipt_log,   # Do not generate iptables logging rules.
    'no-ipt-test'    => \$no_ipt_test,  # Don't perform any checks against
                                        # iptables.
    'no-ipt-jumps'   => \$no_ipt_jumps, # Don't jump packets from the INPUT or
                                        # FORWARD chains.
    'update-rules'   => \$update_rules, # Download latest snort rules.
    'add-deleted'    => \$add_deleted,  # Add deleted rules.
    'strict'         => \$strict,       # Strict mode.
    'debug'          => \$debug,        # Debug mode.
    'dump-conf'      => \$dump_conf,    # Display config variables
    'config=s'       => \$fwsnort_conf, # Manually specify the config file
    'Ulog'           => \$ulog_mode,    # Force ULOG mode.
    'ulog-nlgroup=i' => \$ulog_nlgroup, # Specify the ulogd nl group.
    'verbose'        => \$verbose,
    'logfile=s'      => \$logfile,      # Specify the logfile path.
    'stdout'         => \$stdout,       # Print log messages to stdout.
    'Version'        => \$print_ver,
    'help'           => \$help
));

&usage(0) if $help;

### Print the version number and exit if -V given on the command line.
if ($print_ver) {
    print "[+] fwsnort v$version, by Michael Rash (mbr\@cipherdyne.org)\n";
    exit 0;
}

if ($no_ipt_log && not ($ipt_drop or $ipt_reject)) {
    die "[*] --ipt-no-log option can only be used ",
        "with --ipt-drop or --ipt-reject";
}

if ($ipt_drop and $ipt_reject) {
    die "[*] Cannot specify both --ipt-drop and --ipt-reject";
}

if ($strict) {
    ### make the snort options parser very strict
    $snort_opts{'unsupported'}{'uricontent'}
        = $snort_opts{'filter'}{'uricontent'};
    delete $snort_opts{'filter'}{'uricontent'};
    $snort_opts{'unsupported'}{'offset'}
        = $snort_opts{'ignore'}{'offset'};
    delete $snort_opts{'ignore'}{'offset'};
    $snort_opts{'unsupported'}{'depth'}
        = $snort_opts{'ignore'}{'depth'};
    delete $snort_opts{'ignore'}{'depth'};
    $snort_opts{'unsupported'}{'nocase'}
        = $snort_opts{'ignore'}{'nocase'};
    delete $snort_opts{'ignore'}{'nocase'};
}

$ipt_sync = 0 if $no_ipt_sync;

### make sure some directories exist, etc.
&setup();

### read in configuration info from the config file
&readconf();

### make sure the commands are where the
### config file says they are
&chk_commands();

### make sure the config is correct
&validate_conf();

### validate any networks that were specified on the command line.
&validate_cmdl_networks();

### download latest snort rules from snort.org
&update_rules() if $update_rules;

### if we are running with $chk_ipt_policy, then cache
### the current iptables policy
&cache_ipt_policy() if $ipt_sync;

### check to make sure iptables has --hex-strings, etc.
&ipt_test() unless $no_ipt_test;

### cache all of the fwsnort iptables chains now that
### we know what all of the interfaces are
&build_fwsnort_chains();

### print a header at the top of the iptables ruleset
### script
&ipt_hdr();

### now that we have the interfaces, add the iptables
### chains to the fwsnort shell script
&ipt_add_chains();

### add any ignore rules to each fwsnort chain by using
### the RETURN target
&ipt_ignore_rules();

### display the config on STDOUT
&dump_conf() if $dump_conf;

### make sure <type>.rules file exists if --type was
### specified on the command line
&check_type() if $rules_type;

### truncate old log (does anyone actually use the fwsnort
### parsing log?)
open L, "> $logfile" or die "[*] Could not open $logfile: $!";
close L;

&logr(localtime() . "[+] Begin parsing cycle.");

### parse snort rules (signatures)
if ($ipt_sync) {
    print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
        "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
        sprintf("%-30s%-10s%-10s%-10s%-10s", '    Snort Rules File',
            'Success', 'Fail', 'Ipt_apply', 'Total'), "\n\n";
} else {
    print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
        "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n",
        sprintf("%-30s%-10s%-10s%-10s", '    Snort Rules File',
            'Success', 'Fail', 'Total'), "\n\n";
}

### main subroutine to parse snort rules and add them to the
### fwsnort.sh script.
&parse_snort_rules();

### jump packets (as appropriate) from the INPUT and
### FORWARD chains to our fwsnort chains
&ipt_jump_chain() unless $no_ipt_jumps;

push @ipt_script_lines, "\n### EOF ###";

### write the iptables script out to disk
&write_ipt_script();

chmod 0500, $ipt_script;

print "\n[+] Logfile:         $logfile\n";
print "[+] Iptables script: $ipt_script\n";

if ($ipt_apply) {
    print "[+] Executing $ipt_script\n";
    system $ipt_script;
}
print "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=",
    "-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n";

exit 0;
#===================== end main ======================

sub parse_snort_rules() {
    opendir D, $rules_dir or die "[*] Could not open $rules_dir";
    my @rfiles = readdir D;
    closedir D;
    shift @rfiles; shift @rfiles;

    my $abs_num  = 0;
    my $sabs_num = 0;
    my $tot_ipt_apply = 0;
    my $tot_unsup_ctr = 0;
    FILE: for my $rfile (sort @rfiles) {
        next FILE unless $rfile =~ /\.rules$/;
        $ipt_print_type = 0;
        if ($rules_type) {
            next FILE unless $rfile =~ /^${rules_type}\.rules$/;
        }
        if ($rfile eq 'deleted.rules') {
            next FILE unless $add_deleted;
        }
        ($snort_type) = ($rfile =~ /(\S+)\.rules/);
        printf("%-30s", "[+] $rfile");
        &logr("[+] Parsing $rfile");
        open R, "< ${rules_dir}/${rfile}" or die "[*] Could not ",
            "open: ${rules_type}/${rfile}";
        my @lines = <R>;
        close R;
        my $line_num   = 0;
        my $rule_num   = 0;
        my $parsed_ctr = 0;
        my $unsup_ctr  = 0;
        my $ipt_apply  = 0;
        RULE: for my $rule (@lines) {
            chomp $rule;
            $line_num++;
            ### pass == ACCEPT, log == ULOG
            unless ($rule =~ /^\s*alert/ or $rule =~ /^\s*pass/
                    or $rule =~ /^\s*log/) {
                next RULE;
            }
            $rule_num++;  ### keep track of the abs num of rules
            my $rule_hdr;
            my $rule_options;
            $sabs_num++;
            if ($rule =~ m|^(.*?)\s+\((.*)\)|) {
                $rule_hdr     = $1;
                $rule_options = $2;
            } else {  ### don't know what type of snort rule it is
                next RULE;
            }
            ### skip all icmp "Undefined Code" rules; psad properly
            ### handles this, but not fwsnort (see the icmp-info.rules
            ### file).
            if ($rfile =~ /icmp/ and $rule_options =~ /undefined\s+code/i) {
                $unsup_ctr++;
                $tot_unsup_ctr++;
                next RULE;
            }
            my $hdr_href = &parse_rule_hdr($rule_hdr, $line_num);
            unless ($hdr_href) {
                &logr("[-] Unrecognized rule header: \"$rule_hdr\" at " .
                    "line: $line_num.  Skipping rule.");
                $unsup_ctr++;
                $tot_unsup_ctr++;
                next RULE;
            }
            if ($debug) {
                print STDOUT
"[+] Header: Action: $hdr_href->{'action'}, Proto: $hdr_href->{'proto'}, ",
"Src: $hdr_href->{'src'}, Src_p: $hdr_href->{'src_p'}, bidir: ",
"$hdr_href->{'bidir'}, Dst: $hdr_href->{'dst'}, Dst_p: $hdr_href->{'dst_p'}\n";
            }
            my $opts_href = &parse_rule_options($rule_options, $line_num);
            unless ($opts_href) {
                $unsup_ctr++;
                $tot_unsup_ctr++;
                next RULE;
            }

            ### construct the iptables rule and add it to $ipt_script
            my $ipt_rv = &ipt_build($hdr_href, $opts_href, $rule);
            if ($ipt_rv) {
                $ipt_apply++;
                $tot_ipt_apply++;
            }
            $parsed_ctr++;  ### keep track of successfully parsed rules
            $abs_num++;;
        }
        if ($ipt_sync) {
            printf("%-10s%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr,
                $ipt_apply, $rule_num);
        } else {
            printf("%-10s%-10s%-10s\n", $parsed_ctr, $unsup_ctr, $rule_num);
        }
    }
    if ($ipt_sync) {
        printf("%30s", ' ');
        print "=======================================\n";
        printf("%30s%-10s%-10s%-10s%-10s\n", ' ',
            $abs_num, $tot_unsup_ctr, $tot_ipt_apply, $sabs_num);
    } else {
        printf("%30s", ' ');
        print "=============================\n";
        printf("%30s%-10s%-10s%-10s\n", ' ',
            $abs_num, $tot_unsup_ctr, $sabs_num);
    }
    print "\n";
    if ($abs_num) {  ### we parsed at least one rule
        print "[+] Generated iptables rules for $abs_num out of ",
            "$sabs_num signatures: ",
            sprintf("%.2f", $abs_num/$sabs_num*100), "%\n";
    } else {
        print "[+] No rules parsed.\n";
    }
    if ($ipt_sync) {
        print "[+] Found $tot_ipt_apply applicable snort rules to your " .
            "current iptables\n    policy.\n";
    }
    return;
}

sub parse_rule_options() {
    my ($rule_options, $line_num) = @_;

    ### tmp hash we will return
    my %opts;

    if ($snort_sid) {
        if ($rule_options =~ /sid:\s*$snort_sid\s*;/) {
            &logr("[+] matched sid:$snort_sid: $rule_options");
        } else {
            return 0;
        }
    }

    ### get the sid here for logging purposes
    my ($sid) = ($rule_options =~ /$snort_opts{'logprefix'}{'sid'}/);

    for my $opt (keys %{$snort_opts{'unsupported'}}) {
        ### see if we match a regex belonging to an supported option
        if ($rule_options =~ /$snort_opts{'unsupported'}{$opt}/) {
            &logr("[-] SID: $sid  Unsupported option: \"$opt\" at " .
                "line: $line_num.  Skipping rule.");
            return 0;
        }
    }
    if ($rule_options =~ /content\s*:.*content\s*:/) {
        &logr("[-] SID: $sid  Unsupported multiple content fields at " .
            "line: $line_num.  Skipping rule.");
        return 0;
    }
    if ($rule_options =~ /ip_proto\s*:.*ip_proto\s*:/) {
        &logr("[-] SID: $sid  Unsupported multiple ip_proto fields at " .
            "line: $line_num.  Skipping rule.");
        return 0;
    }

    for my $opt (keys %{$snort_opts{'filter'}}) {
        ### see if we match the option regex
        if ($rule_options =~ /$snort_opts{'filter'}{$opt}/) {
            $opts{$opt} = $1;
        }
    }

    for my $opt (keys %{$snort_opts{'logprefix'}}) {
        if ($rule_options =~ /$snort_opts{'logprefix'}{$opt}/) {
            $opts{$opt} = $1;
        }
    }

    while ($rule_options =~ /(\w+):\s*.*?;/g) {
        my $option = $1;
        if (! defined $opts{$option}
            && ! defined $snort_opts{'ignore'}{$option}) {
            &logr("$line_num SID: $sid  bad option: \"$option\" ",
                "-- $rule_options");
        }
    }
    return \%opts;
}

sub parse_rule_hdr() {
    my ($rule_hdr, $line_num) = @_;
    my $bidir = 0;
    my $action = 'alert';  ### default
    if ($rule_hdr =~ /^\s*pass/) {
        $action = 'pass';
    } elsif ($rule_hdr =~ /^\s*log/) {
        $action = 'log';
    }
    if ($rule_hdr =~ m|^\s*\w+\s+(\S+)\s+\$?(\S+)\s+\$?(\S+)
                        \s+(\S+)\s+\$?(\S+)\s+\$?(\S+)|ix) {
        my $proto  = lc($1);
        my $src    = $2;
        my $src_p  = $3;
        my $bidir  = $4;
        my $dst    = $5;
        my $dst_p  = $6;

        unless ($proto =~ /^\w+$/) {
            &logr("[-] Unsupported protocol: \"$proto\" at line: " .
                "$line_num.  Skipping rule.");
            return 0;
        }
        if ($proto eq 'ip') {
            &logr("[-] Unsupported protocol: \"$proto\" at line: " .
                "$line_num.  Skipping rule.");
            return 0;
        }

        my $bidir_flag = 0;
        $bidir_flag = 1 if $bidir eq '<>';
        return {
            'action' => $action,
            'proto'  => $proto,
            'src'    => $src,
            'src_p'  => $src_p,
            'bidir'  => $bidir_flag,
            'dst'    => $dst,
            'dst_p'  => $dst_p,
        };
    } else {
        return 0;
    }
}

sub build_fwsnort_chains() {
    ### note that the FORWARD chain is not tied to a
    ### specific interface
    $fwsnort_chains{'INPUT'}{$config{'EXTERNAL_INTF'}}
        = "fwsnort_INPUT_$config{'EXTERNAL_INTF'}";

    $fwsnort_chains{'INPUT'}{$config{'INTERNAL_INTF'}}
        = "fwsnort_INPUT_$config{'INTERNAL_INTF'}"
        if defined $config{'INTERNAL_INTF'};

    $fwsnort_chains{'INPUT'}{$config{'DMZ_INTF'}}
        = "fwsnort_INPUT_$config{'DMZ_INTF'}"
        if defined $config{'DMZ_INTF'};

    return;
}

sub ipt_allow_traffic() {
    my ($hdr_href, $chain) = @_;
    my $rv = 0;
    ### check to see if the header is allowed through the
    ### INPUT chain
    my $h_proto = $hdr_href->{'proto'};

    if ($h_proto eq 'icmp') {
        if (defined $ipt_policy{$chain}
                and defined $ipt_policy{$chain}{'icmp'}) {
            return 1;
        }
        return 0;
    }

    my $h_src = $hdr_href->{'src'};
    my $h_dst = $hdr_href->{'dst'};
    my $h_src_p;
    my $h_dst_p;
    if (defined $config{$hdr_href->{'src_p'}}) {
        $h_src_p = $config{$hdr_href->{'src_p'}};
    } else {
        $h_src_p = $hdr_href->{'src_p'};
    }
    if (defined $config{$hdr_href->{'dst_p'}}) {
        $h_dst_p = $config{$hdr_href->{'dst_p'}};
    } else {
        $h_dst_p = $hdr_href->{'dst_p'};
    }
    for my $proto (keys %{$ipt_policy{$chain}}) {
        next unless $h_proto eq $proto;
        for my $src_p (keys %{$ipt_policy{$chain}{$proto}}) {
            if (&match_port($src_p, $h_src_p)) {
                for my $dst_p (keys %{$ipt_policy{$chain}{$proto}{$src_p}}) {
                    if (&match_port($dst_p, $h_dst_p)) {
                        return 1;
                    }
                }
            }
        }
    }
    return 0;
}

sub match_port() {
    my ($ipt_port, $h_port) = @_;
    return 1 if $ipt_port eq '0:0';
    return 1 if $ipt_port eq $h_port;
    return 1 if $h_port eq 0; ### bad traffic (port 0)
    if ($ipt_port =~ /(\d+):(\d+)/) {
        my $s = $1;
        my $e = $2;
        if ($ipt_port !~ /!/) {
            return 1 if $h_port > $s && $h_port < $e;
        } else {
            return 1 if $h_port < $s || $h_port > $e;
        }
    } elsif ($ipt_port =~ /(\d+):/) {
        my $s = $1;
        my $e = 65535;
        if ($ipt_port !~ /!/) {
            return 1 if $h_port > $s && $h_port < $e;
        } else {
            return 1 if $h_port < $s || $h_port > $e;
        }
    } elsif ($ipt_port =~ /:(\d+)/) {
        my $s = 65535;
        my $e = $1;
        if ($ipt_port !~ /!/) {
            return 1 if $h_port > $s && $h_port < $e;
        } else {
            return 1 if $h_port < $s || $h_port > $e;
        }
    } elsif ($ipt_port =~ /!(\d+)/) {
        return 1 if $h_port ne $1;
    }
    return 0;
}

sub cache_ipt_policy() {
    my $ipt = new IPTables::Parse;
    $ipt_policy{'INPUT'} = $ipt->chain_action_rules('filter',
        'INPUT', 'ACCEPT');
    ### we know that the dmz interface can only be defined if we have
    ### already defined the internal inteface.
    if (defined $config{'INTERNAL_INTF'}
            && $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
        $ipt_policy{'FORWARD'} = $ipt->chain_action_rules('filter',
            'FORWARD', 'ACCEPT');
    }
    return;
}

sub ipt_build() {
    my ($hdr_href, $opts_href, $orig_snort_rule) = @_;

    ### for iptables INPUT chain
    my @input_rules_sd   = ();
    my @input_rules_chns = ();

    ### for iptables FORWARD chain
    my @forward_rules_sd  = ();
    my @forward_rules_chn = ();

    my $src = $hdr_href->{'src'};
    my $dst = $hdr_href->{'dst'};

    if (defined $config{$dst} and defined $intf_net{$config{$dst}}) {
        if ($intf_type{$intf_net{$config{$dst}}{'name'}}
                eq 'INTERNAL_INTF') {
            ### snort:      EXTERNAL   -> HOME
            ### iptables:   any source -> external intf  (for INPUT chain)
            push @input_rules_sd, $intf_net{'EXTERNAL_INTF'}{'ip'};
            push @input_rules_chns,
                "fwsnort_INPUT_${intf_net{'EXTERNAL_INTF'}{'name'}}";

            ### snort:      EXTERNAL   -> HOME
            ### iptables:   any source -> internal net (for FORWARD chain)
            if (defined $config{'INTERNAL_INTF'}
                    and $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
                push @forward_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'INTERNAL_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
            if (defined $config{'DMZ_INTF'}) {
                push @forward_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'DMZ_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
        } elsif ($intf_type{$intf_net{$config{$dst}}{'name'}}
                eq 'EXTERNAL_INTF' and defined $config{'INTERNAL_INTF'}) {
            ### snort:      INTERNAL   -> EXTERNAL
            ### iptables:   any source -> internal intf  (for INPUT chain)
            push @input_rules_sd, $intf_net{'INTERNAL_INTF'}{'ip'};
            push @input_rules_chns,
                "fwsnort_INPUT_${intf_net{'INTERNAL_INTF'}{'name'}}";

            ### snort:      HOME -> EXTERNAL
            ### iptables:   internal net -> any destination (for FORWARD chain)
            if ($config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
                push @forward_rules_sd, "$iptables_opts{'src'} " .
                    $intf_net{'INTERNAL_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
            if (defined $config{'DMZ_INTF'}) {
                push @input_rules_sd, $intf_net{'DMZ_INTF'}{'ip'};
                push @input_rules_chns,
                    "fwsnort_INPUT_${intf_net{'DMZ_INTF'}{'name'}}";
                push @forward_rules_sd, "$iptables_opts{'src'} " .
                    $intf_net{'DMZ_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
        }
    } elsif ($dst eq 'any') {
        if ($src eq 'any') {
            ### traffic in the INPUT chain on _both_ interfaces
            push @input_rules_sd, "$iptables_opts{'dst'} " .
                $intf_net{'EXTERNAL_INTF'}{'ip'};
            push @input_rules_chns, "fwsnort_INPUT_$config{'EXTERNAL_INTF'}";

            if (defined $config{'INTERNAL_INTF'}) {
                push @input_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'INTERNAL_INTF'}{'ip'};
                push @input_rules_chns,
                    "fwsnort_INPUT_$config{'INTERNAL_INTF'}";
            }
            if (defined $config{'DMZ_INTF'}) {
                push @input_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'DMZ_INTF'}{'ip'};
                push @input_rules_chns,
                    "fwsnort_INPUT_$config{'DMZ_INTF'}";
            }
            ### traffic through the FORWARD chain originating from either
            ### the internal or external nets
            if (defined $config{'INTERNAL_INTF'}
                    and $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
                push @forward_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'INTERNAL_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
                push @forward_rules_sd, "$iptables_opts{'src'} " .
                    $intf_net{'INTERNAL_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
            if (defined $config{'DMZ_INTF'}) {
                push @forward_rules_sd, "$iptables_opts{'dst'} " .
                    $intf_net{'DMZ_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
                push @forward_rules_sd, "$iptables_opts{'src'} " .
                    $intf_net{'DMZ_INTF'}{'net'};
                push @forward_rules_chn, 'fwsnort_FORWARD';
            }
        } else {
            ### FIXME
#            print " ** src: $src, dst: $dst\n";
        }
    } else {
        ### FIXME
#        print " ** src: $src, dst: $dst\n";
    }

    my $ctr = 0;

    ### build iptables INPUT rules
    my $found_rule = 0;
    for my $sd (@input_rules_sd) {
        ### build as many iptables rules as we need based on
        ### the source and destination
        my $rv = &ipt_build_rule(
            'INPUT',
            "\$IPTABLES -A $input_rules_chns[$ctr]",
            $hdr_href,
            $opts_href,
            $sd,
            $orig_snort_rule,
        );
        $found_rule = 1 if $rv;
        $ctr++;
    }

    $ctr = 0;
    ### build iptables FORWARD rules
    if (defined $config{'INTERNAL_INTF'}
            or defined $config{'DMZ_INTF'}) {
        for my $sd (@forward_rules_sd) {
            ### build as many iptables rules as we need based on
            ### the source and destination
            my $rv = &ipt_build_rule(
                'FORWARD',
                "\$IPTABLES -A $forward_rules_chn[$ctr]",
                $hdr_href,
                $opts_href,
                $sd,
                $orig_snort_rule,
            );
            $found_rule = 1 if $rv;
            $ctr++;
        }
    }
    return $found_rule;
}

sub ipt_build_rule() {
    my ($chain, $rule, $hdr_href, $opts_href,
        $src_dst, $orig_snort_rule) = @_;

    ### $chain is either INPUT or FORWARD, and is used only to
    ### see whether or not we need to add the rule to the iptables
    ### script based on whether the build-in chain will pass the
    ### traffic in the first place.
    if ($ipt_sync) {
        if (! &ipt_allow_traffic($hdr_href, $chain)) {
            return 0;
        }
    }

    ### append the protocol to the rule
    if (defined $opts_href->{'ip_proto'}) {
        return 0 unless $opts_href->{'ip_proto'} =~ /^\w+$/;
        $rule .= " $iptables_opts{'ip_proto'} $opts_href->{'ip_proto'}";
    } else {
        return 0 unless $hdr_href->{'proto'} =~ /^\w+$/;
        $rule .= " $iptables_opts{'proto'} $hdr_href->{'proto'}";
    }

    ### append the source (this will always be either "-s <net>" or
    ### "-d <net>" instead of "-s <ip>" or "-d <ip>" (the IP tied to
    ### a firewall interface is never actually used since everything
    ### is done by interface instead).  This is definitely a kludge.
    if ($src_dst =~ m|$iptables_opts{'src'}|) {
        $rule .= " $src_dst";
    }

    ### append the source port
    if (defined $config{$hdr_href->{'src_p'}}) {
        $config{$hdr_href->{'src_p'}} =~ s/\!(\d)/! $1/;
        $rule .= " $iptables_opts{'src_p'} $config{$hdr_href->{'src_p'}}";
    } elsif ($hdr_href->{'src_p'} ne 'any') {
        $hdr_href->{'src_p'} =~ s/\!(\d)/! $1/;
        $rule .= " $iptables_opts{'src_p'} $hdr_href->{'src_p'}";
    }

    ### append the destination
    if ($src_dst =~ m|$iptables_opts{'dst'}|) {
        $rule .= " $src_dst";
    }

    ### append the destination port
    if (defined $config{$hdr_href->{'dst_p'}}) {
        $config{$hdr_href->{'dst_p'}} =~ s/\!(\d)/! $1/;
        $rule .= " $iptables_opts{'dst_p'} $config{$hdr_href->{'dst_p'}}";
    } elsif ($hdr_href->{'dst_p'} ne 'any') {
        $hdr_href->{'dst_p'} =~ s/\!(\d)/! $1/;
        $rule .= " $iptables_opts{'dst_p'} $hdr_href->{'dst_p'}";
    }

    &ipt_build_opts($rule, $hdr_href, $opts_href, $orig_snort_rule);
    return 1;
}

sub ipt_build_opts() {
    my ($rule, $hdr_href, $opts_href, $orig_snort_rule) = @_;

    ### append tcp flags
    if (defined $opts_href->{'flags'}) {
        my $f_str = '';

        $f_str .= 'URG,' if $opts_href->{'flags'} =~ /U/i;
        $f_str .= 'ACK,' if $opts_href->{'flags'} =~ /A/i;
        $f_str .= 'PSH,' if $opts_href->{'flags'} =~ /P/i;
        $f_str .= 'RST,' if $opts_href->{'flags'} =~ /R/i;
        $f_str .= 'SYN,' if $opts_href->{'flags'} =~ /S/i;
        $f_str .= 'FIN,' if $opts_href->{'flags'} =~ /F/i;
        $f_str =~ s/\,$//;

        if ($opts_href->{'flags'} =~ /\+/) {
            ### --tcp-flags ACK ACK
            $rule .= " $iptables_opts{'flags'} $f_str $f_str";
        } else {
            ### --tcp-flags ALL URG,PSH,SYN,FIN
            $rule .= " $iptables_opts{'flags'} ALL $f_str";
        }
    }

    ### append --tcp-flags ACK ACK if flow=established.
    ### NOTE: we can't really handle "flow" in the same way snort can,
    ### since there is no way to keep track of which side initiated the
    ### tcp session (where the SYN packet came from), but older versions
    ### of snort (pre 1.9) just used tcp flags "A+" to keep track of
    ### this... we need to do the same.
    if (defined $opts_href->{'flow'} && ! defined $opts_href->{'flags'}) {
        if ($opts_href->{'flow'} =~ /established/) {
            ### note that this ignores the "stateless" keyword
            ### as it should...
            $rule .= " $iptables_opts{'flow'} ACK ACK";
        }
    }

    ### append tos
    if (defined $opts_href->{'tos'}) {
        $rule .= " $iptables_opts{'tos'} $opts_href->{'tos'}"
    }

    ### append ttl
    if (defined $opts_href->{'ttl'}) {
        if ($opts_href->{'ttl'} =~ /\<\s*(\d+)/) {
            $rule .= " $iptables_opts{'ttl'} -ttl-lt $1";
        } elsif ($opts_href->{'ttl'} =~ /\>\s*(\d+)/) {
            $rule .= " $iptables_opts{'ttl'} -ttl-gt $1";
        } else {
            $rule .= " $iptables_opts{'ttl'} -ttl-eq $opts_href->{'ttl'}";
        }
    }

    ### append icmp type
    if (defined $opts_href->{'itype'}) {
        $rule .= " $iptables_opts{'itype'} $opts_href->{'itype'}"
    }

    ### append ip options
    if (defined $opts_href->{'ipopts'}) {
        $rule .= " $iptables_opts{'ipopts'} --$opts_href->{'ipopts'}"
    }

    ### append snort content options
    my $content = '';
    if (defined $opts_href->{'uricontent'}) {
        $content = $opts_href->{'uricontent'};
    } elsif (defined $opts_href->{'content'}) {
        $content = $opts_href->{'content'};
    }
    if ($content) {
        $content =~ s/`/\\`/g;
        if ($content =~ /\|.+\|/) {  ### there is hex data in the content
            $rule .= " $iptables_opts{'content'} --hex-string \"$content\"";
        } else {
            $rule .= " $iptables_opts{'content'} --string \"$content\"";
        }
    }

    my $log_target = '';
    if ($hdr_href->{'action'} eq 'log' or $ulog_mode) {
        $log_target = " -j ULOG --ulog-nlgroup $ulog_nlgroup " .
            "--ulog-prefix \"SID$opts_href->{'sid'} \""
            if defined $opts_href->{'sid'};
    } else {
        ### construct the log-prefix (should only contain the sid
        ### to conserve kernel memory).
        $log_target = " -j LOG --log-prefix \"SID$opts_href->{'sid'} \""
            if defined $opts_href->{'sid'};
    }

    ### print the rest of the logprefix snort options in a comment
    ### one line above the rule
    my $comment = '###';
    for my $key qw(msg classtype reference priority rev) {
        if (defined $opts_href->{$key}) {
            if ($key eq 'msg') {
                $comment .= qq| $key: "$opts_href->{$key}";|;
            } else {
                $comment .= qq| $key: $opts_href->{$key};|;
            }
        }
    }
    $comment =~ s/,$//;

    ### print the snort rules type header to the fwsnort.sh script
    if (! $ipt_print_type) {
        &ipt_type($snort_type);
        $ipt_print_type = 1;
    }

    ### write the rule out to the iptables script
    &ipt_add_rule($hdr_href, $opts_href, $orig_snort_rule,
        $rule, $log_target, $comment);
    return;
}

sub ipt_add_rule() {
    my ($hdr_href, $opts_href, $orig_snort_rule, $rule_base,
        $log_target, $comment) = @_;

    my $action_rule = '';
    if ($hdr_href->{'proto'} eq 'tcp') {
        if ($hdr_href->{'action'} eq 'pass') {
            $action_rule = "$rule_base -j ACCEPT";
        } else {
            if (defined $opts_href->{'resp'}
                    and $opts_href->{'resp'} =~ /rst/i) {
                ### Netfilter can only send tcp resets to the connection
                ### client, so we can't support rst_rcv, but we should
                ### try to tear the connection down anyway.
                $action_rule = "$rule_base -j REJECT " .
                    "--reject-with tcp-reset";
            } elsif ($ipt_drop) {
                $action_rule = "$rule_base -j DROP";
            } elsif ($ipt_reject) {
                $action_rule = "$rule_base -j REJECT " .
                    "--reject-with tcp-reset";
            }
        }
    } elsif ($hdr_href->{'proto'} eq 'udp') {
        if ($hdr_href->{'action'} eq 'pass') {
            $action_rule = "$rule_base -j ACCEPT";
        } else {
            if (defined $opts_href->{'resp'}
                    and $opts_href->{'resp'} =~ /icmp/i) {
                if ($opts_href->{'resp'} =~ /all/i) {  ### icmp_all
                    $action_rule = "$rule_base -j REJECT " .
                        "--reject-with icmp-port-unreachable";
                } elsif ($opts_href->{'resp'} =~ /net/i) {  ### icmp_net
                    $action_rule = "$rule_base -j REJECT " .
                        "--reject-with icmp-net-unreachable";
                } elsif ($opts_href->{'resp'} =~ /host/i) {  ### icmp_host
                    $action_rule = "$rule_base -j REJECT " .
                        "--reject-with icmp-host-unreachable";
                } elsif ($opts_href->{'resp'} =~ /port/i) {  ### icmp_port
                    $action_rule = "$rule_base -j REJECT " .
                        "--reject-with icmp-port-unreachable";
                }
            } elsif ($ipt_drop) {
                $action_rule = "$rule_base -j DROP";
            } elsif ($ipt_reject) {
                $action_rule = "$rule_base -j REJECT " .
                    "--reject-with icmp-port-unreachable";
            }
        }
    } else {
        if ($hdr_href->{'action'} eq 'pass') {
            $action_rule = "$rule_base -j ACCEPT";
        } else {
            $action_rule = "$rule_base -j DROP";
        }
    }
    my $log_rule = $rule_base . $log_target;

    if ($verbose) {
        push @ipt_script_lines, "### snort rule: $orig_snort_rule";
        push @ipt_script_lines, "\$ECHO \"[+] rule $ipt_rule_ctr\"";
    } else {
        push @ipt_script_lines, $comment;
    }
    if ($hdr_href->{'action'} ne 'pass') {
        push @ipt_script_lines, $log_rule unless $no_ipt_log;
    }
    push @ipt_script_lines, $action_rule
        if $action_rule and ($ipt_drop or $ipt_reject or
            $hdr_href->{'action'} eq 'pass' or defined $opts_href->{'resp'});
    $ipt_rule_ctr++;
    return;
}

sub ipt_ignore_rules() {
    if (@ignore_ips or @ignore_nets) {
        push @ipt_script_lines, "\n###\n############ Add IP/network " .
            "ignore rules. ############\n###";
    } else {
        return;
    }
    for my $type (keys %fwsnort_chains) {
        for my $intf (keys %{$fwsnort_chains{$type}}) {
            for my $ip (@ignore_ips) {
                push @ipt_script_lines, "\$IPTABLES -A " .
                    "$fwsnort_chains{$type}{$intf} -s $ip -j RETURN";
            }
            for my $net (@ignore_nets) {
                push @ipt_script_lines, "\$IPTABLES -A " .
                    "$fwsnort_chains{$type}{$intf} -s $net -j RETURN";
            }
        }
    }
    my $added_forward = 0;
    if (defined $config{'INTERNAL_INTF'}
            && $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
        for my $ip (@ignore_ips) {
            push @ipt_script_lines, "\$IPTABLES -A fwsnort_FORWARD " .
                "-s $ip -j RETURN";
        }
        for my $net (@ignore_nets) {
            push @ipt_script_lines, "\$IPTABLES -A fwsnort_FORWARD " .
                "-s $net -j RETURN";
        }
        $added_forward = 1;
    }
    if (defined $config{'DMZ_INTF'} && ! $added_forward) {
        for my $ip (@ignore_ips) {
            push @ipt_script_lines, "\$IPTABLES -A fwsnort_FORWARD " .
                "-s $ip -j RETURN";
        }
        for my $net (@ignore_nets) {
            push @ipt_script_lines, "\$IPTABLES -A fwsnort_FORWARD " .
                "-s $net -j RETURN";
        }
    }
    return;
}

sub ipt_add_chains() {
    push @ipt_script_lines, "\n###\n############ Create " .
        "fwsnort iptables chains. ############\n###";
    for my $type (keys %fwsnort_chains) {
        for my $intf (keys %{$fwsnort_chains{$type}}) {
            push @ipt_script_lines, "\$IPTABLES -N " .
                "$fwsnort_chains{$type}{$intf} 2> /dev/null";
            push @ipt_script_lines, "\$IPTABLES -F " .
                "$fwsnort_chains{$type}{$intf}\n";
        }
    }
    my $added_forward = 0;
    if (defined $config{'INTERNAL_INTF'}
            && $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
        push @ipt_script_lines, "\$IPTABLES -N fwsnort_FORWARD 2> /dev/null";
        push @ipt_script_lines, "\$IPTABLES -F fwsnort_FORWARD\n";
        $added_forward = 1;

    }
    if (defined $config{'DMZ_INTF'} && ! $added_forward) {
        push @ipt_script_lines, "\$IPTABLES -N fwsnort_FORWARD 2> /dev/null";
        push @ipt_script_lines, "\$IPTABLES -F fwsnort_FORWARD\n";
    }
    return;
}

sub ipt_jump_chain() {
    push @ipt_script_lines, "\n###\n############ Jump traffic " .
        "to the fwsnort chains. ############\n###";
    for my $type (keys %fwsnort_chains) {
        for my $intf (keys %{$fwsnort_chains{$type}}) {
            push @ipt_script_lines, "\$IPTABLES -I $type 1 -i $intf " .
                "-j $fwsnort_chains{$type}{$intf}";
        }
    }
    my $added_jump = 0;
    if (defined $config{'INTERNAL_INTF'}
        && $config{'INTERNAL_INTF'} ne $config{'EXTERNAL_INTF'}) {
        push @ipt_script_lines, "\$IPTABLES -I FORWARD 1 -j fwsnort_FORWARD\n";
        $added_jump = 1;
    }
    if (defined $config{'DMZ_INTF'} && ! $added_jump) {
        push @ipt_script_lines, "\$IPTABLES -I FORWARD 1 -j fwsnort_FORWARD\n";
    }
    return;
}

sub ipt_hdr() {
    push @ipt_script_lines, "#!$cmds{'sh'}\n#";
    push @ipt_script_lines, "##########################################" .
        "##############################";
    push @ipt_script_lines, "#\n# File:  $ipt_script";
    push @ipt_script_lines, "#\n# Purpose:  This script was auto " .
        "generated by fwsnort, and implements";
    push @ipt_script_lines, "#           an iptables ruleset based upon " .
        "Snort rules.  For more";
    push @ipt_script_lines, "#           information see the fwsnort man " .
        "page or the documentation";
    push @ipt_script_lines, "#           available at " .
        "http://www.cipherdyne.org/fwsnort/";
    push @ipt_script_lines, "#\n# Author:  Michael Rash <mbr\@cipherdyne.org>";
    push @ipt_script_lines, "#";
    push @ipt_script_lines, "###############################################" .
        "#########################\n#\n";

    ### add paths to system binaries (iptables included)
    &ipt_config_section();
    return;
}

sub ipt_config_section() {
    ### build the config section of the iptables script
    push @ipt_script_lines, "#==================== config ====================";
    push @ipt_script_lines, "ECHO=$cmds{'echo'}";
    push @ipt_script_lines, "IPTABLES=$cmds{'iptables'}";
    push @ipt_script_lines, "#================== end config ==================\n";
    return;
}

sub ipt_type() {
    my $type = shift;
    push @ipt_script_lines, "\n###\n############ ${type}.rules #######" .
        "#####\n###";
    push @ipt_script_lines, "\$ECHO \"[+] Adding $type rules.\"";
    return;
}

sub check_type() {
    unless (-e "${rules_dir}/${rules_type}.rules") {
        print "[-] \"$rules_type\" is not a valid type.\n",
            "    Choose from the following available signature types:\n";
        opendir D, $rules_dir or die "[*] Could not open $rules_dir";
        my @rfiles = readdir D;
        closedir D;
        shift @rfiles; shift @rfiles;
        for my $file (@rfiles) {
            if ($file =~ /^(\S+)\.rules/) {
                print "        $1\n";
            }
        }
        print "[-] Exiting.\n";
        exit 1;
    }
    return;
}

sub readconf() {
    open C, "< $fwsnort_conf" or die $!;
    my @lines = <C>;
    close C;
    my $l_ctr = 0;
    for my $line (@lines) {
        $l_ctr++;
        chomp $line;
        next if $line =~ /^\s*#/;
        next unless $line =~ /\S/;
        if ($line =~ /^\s*(\w+)Cmd\s+(\S+);/) {  ### e.g. "iptableCmd"
            $cmds{$1} = $2;
        } elsif ($line =~ /^\s*(\S+)\s+(.*?);/) {
            my $var = $1;
            my $val = $2;
            die "[*] $fwsnort_conf: Variable \"$var\" is set to\n",
                "    _CHANGEME_ at line $l_ctr.  Edit $fwsnort_conf.\n"
                if $val eq '_CHANGEME_';
            if ($var eq 'IGNOREIP') {
                if ($val =~ /^$ip_re$/) {
                    push @ignore_ips, $val;
                } else {
                    die "[*] $fwsnort_conf: IGNOREIP must be set to a \n",
                        "single IP address.\n";
                }
            } elsif ($var eq 'IGNORENET') {
                if ($val =~ m|^$ip_re/\d+$|) {
                    push @ignore_nets, $val;
                } else {
                    die "[*] $fwsnort_conf: IGNORENET must be set to a \n",
                        "CIDR network (e.g. \"192.168.10.0/24\").\n";
                }
            } else {
                $config{$var} = $val;
            }
        }
    }
    return;
}

sub validate_conf() {
    die "[*] The path to ifconfig is not in $fwsnort_conf. Exiting.\n"
        unless defined $cmds{'ifconfig'};

    die "[*] $fwsnort_conf: EXTERNAL_INTF is not defined. Exiting.\n"
        unless defined $config{'EXTERNAL_INTF'};
    die "[*] $fwsnort_conf: INTERNAL_INTF is not defined. Exiting.\n"
        unless defined $config{'INTERNAL_INTF'};

    $intf_type{$config{'EXTERNAL_INTF'}} = 'EXTERNAL_INTF';
    $intf_net{'EXTERNAL_INTF'}{'name'} = $config{'EXTERNAL_INTF'};

    $intf_type{$config{'INTERNAL_INTF'}} = 'INTERNAL_INTF';
    $intf_net{'INTERNAL_INTF'}{'name'} = $config{'INTERNAL_INTF'};

    ### take the internal network from the command line if --internal-net was
    ### given (NOTE: the network tied to the external interface is never really
    ### used; the iptables rules are built instead with a source of the
    ### internal network).
    if ($int_net) {
        $intf_net{'INTERNAL_INTF'}{'ip'} = '';
        $intf_net{'INTERNAL_INTF'}{'net'} = $int_net;
        $intf_net{'EXTERNAL_INTF'}{'ip'} = '';
        $intf_net{'EXTERNAL_INTF'}{'net'} = '';  ### net is never used
    } else {
        ### read the networks off of the interfaces
        ($intf_net{'INTERNAL_INTF'}{'ip'}, $intf_net{'INTERNAL_INTF'}{'net'})
            = &get_intf_net($config{'INTERNAL_INTF'});
        ($intf_net{'EXTERNAL_INTF'}{'ip'}, $intf_net{'EXTERNAL_INTF'}{'net'})
            = &get_intf_net($config{'EXTERNAL_INTF'});
    }
    if (defined $config{'DMZ_INTF'}) {
        if (not defined $config{'INTERNAL_INTF'}) {
            die "[*] $fwsnort_conf: DMZ_INTF cannot be defined without ",
                "also defining INTERNAL_INTF. Exiting.\n";
        }
        if ($config{'DMZ_INTF'} eq $config{'INTERNAL_INTF'}
                || $config{'DMZ_INTF'} eq $config{'EXTERNAL_INTF'}) {
            die "[*] $fwsnort_conf: DMZ_INTF cannot be either the internal ",
                "or external interface.  Exiting.\n";
        }
        $intf_type{$config{'DMZ_INTF'}} = 'DMZ_INTF';
        $intf_net{'DMZ_INTF'}{'name'} = $config{'DMZ_INTF'};
        if ($dmz_net) {
            $intf_net{'DMZ_INTF'}{'ip'} = '';
            $intf_net{'DMZ_INTF'}{'net'} = $dmz_net;
        } else {
            ($intf_net{'DMZ_INTF'}{'ip'}, $intf_net{'DMZ_INTF'}{'net'})
                = &get_intf_net($config{'DMZ_INTF'});
        }
    }

    ### build ip arrays for the server variables
    &build_ip_arr('HTTP_SERVERS', \@http_servers);
    &build_ip_arr('SMTP_SERVERS', \@smtp_servers);
    &build_ip_arr('DNS_SERVERS', \@dns_servers);
    &build_ip_arr('SQL_SERVERS', \@sql_servers);
    &build_ip_arr('TELNET_SERVERS', \@telnet_servers);
    &build_ip_arr('AIM_SERVERS', \@aim_servers);

    ### build port arrays for the port variables
    &build_port_arr('HTTP_PORTS', \@http_ports);
    &build_port_arr('SHELLCODE_PORTS', \@shellcode_ports);
    &build_port_arr('ORACLE_PORTS', \@oracle_ports);

    ### make sure all necessary iptables options are defined
    ### based on the options in %snort_opts
    for my $snort_opt (keys %{$snort_opts{'filter'}}) {
        die "[*] Equivalent iptables filter option not defined\n",
            "    for snort option \"$snort_opt\""
        unless defined $iptables_opts{$snort_opt};
    }

    return;
}

sub validate_cmdl_networks() {
    ### we already know that both EXTERNAL_INTF and INTERNAL_INTF
    ### are defined from validate_conf().
    &validate_subnet($int_net, '-i') if $int_net;

    if ($dmz_net) {
        die "[*] Must define DMZ_INTF in $fwsnort_conf to specify a DMZ net.\n"
            unless defined $config{'DMZ_INTF'};
        &validate_subnet($dmz_net, '-d');
    }
    return;
}

sub validate_subnet() {
    my ($net, $cmdl_opt) = @_;
    if ($net =~ m|^$ip_re/(\S+)|) {
        my $mask = $1;
        if ($mask =~ /^\d+$/) {
            unless ($mask > 7 and $mask < 33) {
                die "[*] CIDR mask must be in the range 8-32 with $cmdl_opt\n";
            }
        } else {
            unless ($mask =~ /$ip_re/) {
                die "[*] Netmask must either be in CIDR or regular notation.\n";
            }
        }
    } else {
        die "[*] Must specify network as <net>/<mask> with $cmdl_opt\n";
    }
    return;
}

sub ipt_test() {

    ### test for the LOG target.
    my $rv = (system "$cmds{'iptables'} -I INPUT 1 -s " .
        "127.0.0.2 -j LOG 2> /dev/null") >> 8;
    if ($rv == 0) {
        system "$cmds{'iptables'} -D INPUT 1";
    } else {
        die "[*] Iptables has not been compiled with logging support.  ",
            "If you want to\n    have fwsnort generate an iptables script ",
            "    anyway then specify the\n    --no-ipt-test option. ",
            "Exiting.\n"
            unless $no_ipt_log;
    }

    ### test for the ipv4options extension.
    $rv = (system "$cmds{'iptables'} -I INPUT 1 -p icmp --icmp-type 0 -m " .
        "ipv4options --rr -s 127.0.0.2 -j LOG 2> /dev/null") >> 8;
    if ($rv == 0) {
        system "$cmds{'iptables'} -D INPUT 1";
    } else {
        ### put ipopts in the unsupported list
        $snort_opts{'unsupported'}{'ipopts'} = '[\s;]ipopts:\s*(\w+)\s*;';
    }

    ### test for the ttl extension.
    $rv = (system "$cmds{'iptables'} -I INPUT 1 -p icmp -s 127.0.0.2 " .
        "-m ttl --ttl 1 --icmp-type 8 -j LOG 2> /dev/null") >> 8;
    if ($rv == 0) {
        system "$cmds{'iptables'} -D INPUT 1";
    } else {
        ### put ttl in the unsupported list
        $snort_opts{'unsupported'}{'ttl'} = '[\s;]ttl:\s*(.*?)\s*;';
    }

    ### test for string match support.
    $rv = (system "$cmds{'iptables'} -I INPUT 1 -s " .
        qq|127.0.0.2 -m string --string "test" 2> /dev/null|) >> 8;
    if ($rv == 0) {
        system "$cmds{'iptables'} -D INPUT 1";
    } else {
        die "[*] It does not appear that string match support is ",
            "compiled into\n    iptables.  fwsnort will not be of very ",
            "much use without this.\n    ** NOTE: If you want to have ",
            "fwsnort generate an iptables script\n    anyway, ",
            "specify the --no-ipt-test option.  Exiting.\n";
    }

    ### test for --hex-string
    $rv = (system "$cmds{'iptables'} -I INPUT 1 -s 127.0.0.2 " .
        "-m string --hex-string \"|0a 5d|\" 2> /dev/null") >> 8;
    if ($rv == 0) {
        system "$cmds{'iptables'} -D INPUT 1";
    } else {
        die "[*] It does not appear that the --hex-string patch has ",
            "been applied.\n    fwsnort will not be of very much use ",
            "without this.\n    ** NOTE: If you want to have ",
            "fwsnort generate an iptables script\n    anyway, ",
            "specify the --no-ipt-test option.  Exiting.\n";
    }

    if ($ipt_reject) {
        ### we are going to generate a policy that drops icmp and udp
        ### packets, and kills tcp sessions with tcp-reset.
        $rv = (system "$cmds{'iptables'} -I INPUT 1 -p tcp -s 127.0.0.2 " .
            "-j REJECT --reject-with tcp-reset 2> /dev/null") >> 8;
        if ($rv == 0) {
            system "$cmds{'iptables'} -D INPUT 1";
        } else {
            die "[*] It does not appear that the REJECT target has ",
                "been compiled into iptables.\n    The --ipt-reject option ",
                "requires this option so that tcp sessions can be killed.\n",
                "    Exiting.\n";
        }
    }

    ### more tests should be added
    return;
}

sub build_port_arr() {
    my ($key, $aref) = @_;
    if (defined $config{$key}) {
        @$aref = split /,\s*/, $config{$key};
        for my $port (@$aref) {
            die "[*] $fwsnort_conf: $key can only contain a comma\n",
                "    separated list of port numbers.\n"
                unless $port =~ /^\d+$/
                or $port =~ /^\!\d+$/
                or $port =~ /^\d+:\d+$/
                or $port =~ /^!\d+:\d+$/;
        }
        $conf_supported_vars{$key} = '';
    }
    return;
}

sub build_ip_arr() {
    my ($key, $aref) = @_;
    if (defined $config{$key}) {
        my $intf_flag = 0;
        for my $loc (keys %intf_net) {
            if ($config{$key} eq $loc) {
                ### the server variable is tied to an interface
                $intf_flag = 1;
            }
        }
        unless ($intf_flag) {
            @$aref = split /,\s*/, $config{$key};
            for my $ip (@$aref) {
                die "[*] $fwsnort_conf: $key can only contain a comma\n",
                    "    separated list of ip addresses.\n"
                    unless $ip =~ /^$ip_re$/
                    or $ip =~ m|^$ip_re/\d+$|;
            }
        }
        $conf_supported_vars{$key} = '';
    }
    return;
}

sub get_intf_net() {
    ### NOTE: the ip associated with an interface is never used by fwsnort
    ### as of version 0.6.2.  Traffic matched against the INPUT or OUTPUT
    ### chains simply uses the interfaces in the iptables rules, and traffic
    ### in the FORWARD chain is matched against the internal network.  We
    ### return the interface IP information just because it is easy to do
    ### since we are already parsing the output of ifconfig.
    my $intf = shift;
    my @if_lines = `$cmds{'ifconfig'} $intf`;
    for my $line (@if_lines) {
        ### this is very generic; the output of ifconfig can vary
        ### quite a bit depending on the specific type of interface
        ### (and even the language output... Italian says "inet adr:")
        if ($line =~ /^\s+inet\s+.*?:($ip_re)\s+.*:($ip_re)/ix) {
            my ($net, $cidr) = ipv4_network("$1/$2");
            return $1, "${net}/${cidr}";
        }
    }
    die "[*] Could not get ip and netmask for interface $intf.  Is the\n",
        "    interface up?\n";
}

sub dump_conf() {
    print "[+] Interfaces:\n";
    for my $loc (keys %intf_net) {
        print "    Type: $loc, Name: $intf_net{$loc}{'name'}, ",
            "Net: $intf_net{$loc}{'net'}\n";
    }
    print "[+] HTTP_SERVERS: ";
    if (defined $config{'HTTP_SERVERS'}) {
        if (@http_servers) {
            print @http_servers, "\n";
        } else {
            print $intf_type{$config{'HTTP_SERVERS'}}, "\n";
        }
    } else {
        print "Not Defined\n";
    }
    print "[+] SMTP_SERVERS: ";
    if (defined $config{'SMTP_SERVERS'}) {
        if (@smtp_servers) {
            print @smtp_servers, "\n";
        } else {
            print $intf_type{$config{'SMTP_SERVERS'}}, "\n";
        }
    } else {
        print "Not Defined\n";
    }
    print "[+] DNS_SERVERS: ";
    if (defined $config{'DNS_SERVERS'}) {
        if (@dns_servers) {
            print @dns_servers, "\n";
        } else {
            print $intf_type{$config{'DNS_SERVERS'}}, "\n";
        }
    } else {
        print "Not Defined\n";
    }
    print "[+] SQL_SERVERS: ";
    if (defined $config{'SQL_SERVERS'}) {
        if (@sql_servers) {
            print @sql_servers, "\n";
        } else {
            print $intf_type{$config{'SQL_SERVERS'}}, "\n";
        }
    } else {
        print "Not Defined\n";
    }
    print "[+] TELNET_SERVERS: ";
    if (defined $config{'TELNET_SERVERS'}) {
        if (@telnet_servers) {
            print @telnet_servers, "\n";
        } else {
            print $intf_type{$config{'TELNET_SERVERS'}}, "\n";
        }
    } else {
        print "Not Defined\n";
    }
    print "[+] AIM_SERVERS: ";
    if (@aim_servers) {
        print @aim_servers, "\n";
    } else {
        print "Not Defined\n";
    }
    print "[+] HTTP_PORTS: ";
    if (@http_ports) {
        print @http_ports, "\n";
    } else {
        print "Not Defined\n";
    }
    print "[+] SHELLCODE_PORTS: ";
    if (@shellcode_ports) {
        print @shellcode_ports, "\n";
    } else {
        print "Not Defined\n";
    }
    print "[+] ORACLE_PORTS: ";
    if (@oracle_ports) {
        print @oracle_ports, "\n";
    } else {
        print "Not Defined\n";
    }
    exit 0;
}

sub setup() {

    ### turn off buffering
    $| = 1;

    ### these two directories must already exist for
    ### things to work
    die "[*] No fwsnort directory $fwsnort_dir: $!"
        unless -d $fwsnort_dir;
    die "[*] No snort rules directory $rules_dir: $!"
        unless -d $rules_dir;

    ### these directories can be created at runtime
    unless (-d $archive_dir) {
        mkdir $archive_dir, 0500 or die $!;
    }

    ### these directories can be created at runtime
    unless (-d $log_dir) {
        mkdir $log_dir, 0755 or die $!;
    }

    ### archive any existing ipt_script file
    &archive($ipt_script);

    return;
}

sub update_rules() {
    ### make sure we can actually reach snort.org.
    print "[+] Downloading latest rules:\n",
        "    http://$bleeding_snort_website/bleeding-all.rules\n";
    chdir $rules_dir or die "[*] Could not chdir $rules_dir: $!";
    if (-e 'bleeding-all.rules') {
        move 'bleeding-all.rules', 'bleeding-all.rules.tmp'
            or die "[*] Could not move bleeding-all.rules -> ",
            "bleeding-all.rules.tmp";
    }
    system "$cmds{'wget'} http://$bleeding_snort_website/bleeding-all.rules";
    if (-e 'bleeding-all.rules') {  ### successful download
        unlink 'bleeding-all.rules.tmp';
    } else {
        print "[-] Could not download bleeding-all.rules file.\n";
        if (-e 'bleeding-all.rules.tmp') {
            ### move the original back
            move 'bleeding-all.rules', 'bleeding-all.rules.tmp'
                or die "[*] Could not move bleeding-all.rules -> ",
                "bleeding-all.rules.tmp";
        }
    }
    print "[+] Finished.\n";
    exit 0;
}

sub chk_commands() {
    for my $cmd (keys %cmds) {
        die "[*] $cmd is not located at $cmds{$cmd}: $!"
            unless -e $cmds{$cmd};
        die "[*] $cmd is not executable at $cmds{$cmd}: $!"
            unless -x $cmds{$cmd};
    }
    return;
}

sub archive() {
    my $file = shift;
    return unless $file =~ m|/|;
    my ($filename) = ($file =~ m|.*/(.*)|);
    my $targetbase = "${archive_dir}/${filename}.old";
    for (my $i = 4; $i > 1; $i--) {  ### keep five copies of the old config files
        my $oldfile = $targetbase . $i;
        my $newfile = $targetbase . ($i+1);
        if (-e $oldfile) {
            move $oldfile, $newfile;
        }
    }
    if (-e $targetbase) {
        my $newfile = $targetbase . '2';
        move $targetbase, $newfile;
    }
    &logr("[+] Archiving $file");
    move $file, $targetbase;   ### move $file into the archive directory
    return;
}

sub write_ipt_script() {
    open F, "> $ipt_script" or die "[*] Could not open $ipt_script: $!";
    print F "$_\n" for @ipt_script_lines;
    close F;
    return;
}

sub logr() {
    my $msg = shift;
    if ($stdout) {
        print STDOUT "$msg\n";
    } else {
        open F, ">> $logfile" or die "[*] Could not open $logfile: $!";
        print F "$msg\n";
        close F;
    }
    return;
}

sub usage() {
    my $exit = shift;
    print <<_USAGE_;

fwsnort v$version
[+] By Michael Rash <mbr\@cipherdyne.org>, http://www.cipherdyne.org/

Usage: fwsnort [-t <snort rule type>] [-l <logfile>] [-v] [-V] [-h]
       [-u] [-c <config file>] [--snort-sid=<sid>] [--ipt-apply]
       [--internal-net <internal network>] [--dmz-net <dmz network>]
       [--ipt-drop] [--ipt-script=<script>] [--no-ipt-sync]
       [--no-ipt-log] [--no-ipt-test] [--no-ipt-jumps] [--dump-conf]
       [--debug] [--strict]

Options:
    --strict                  - Make snort parser very strict about
                                which options it will translate into
                                iptables rules.
    --ipt-script=<script>     - Print iptables script to <script>
                                instead of the normal location at
                                $ipt_script
    --ipt-apply               - Execute the fwsnort.sh script.
    --ipt-reject              - Add a protocol dependent REJECT rule
                                (tcp resets for tcp or icmp port
                                unreachable for udp messages) for
                                every logging rule.
    --ipt-drop                - Add a DROP rule for every logging rule.
    --snort-sid=<sid>         - Generate an equivalent iptables rule
                                for the specific snort id <sid>.
    --no-ipt-sync             - Add iptables rules for signatures that
                                are already blocked by iptables.
    --no-ipt-log              - Do not generate iptables log rules
                                (can only be used with --ipt-drop).
    --no-ipt-test             - Do not run any checks for availability
                                of iptables modules (string, LOG,
                                ttl, etc.).
    --no-ipt-jumps            - Do not jump packets from built-in
                                iptables INPUT or FORWARD chains to
                                chains created by fwsnort.
    --internal-net <net/mask> - Manually specify the internal network
                                (CIDR or standard notation).
    --dmz-net <net/mask>      - Manually specify a dmz network
                                (CIDR or standard notation).
    --update-rules            - Download latest Bleeding-Snort rules
                                from http://$bleeding_snort_website/
    -t   --type=<type>        - Only process snort rules of type <type>
                                (e.g. "ddos" or "backdoor")
    -c   --config=<config>    - Use <config> instead of the normal
                                config file located at
                                $fwsnort_conf
    -l   --logfile=<file>     - Log messages to <file> instead of the
                                normal location at
                                $logfile
    -U   --Ulog               - Force ULOG target for all log generation.
    -ulog-nlgroup=<groupnum>  - Specify a ULOG netlink group (the default
                                is 1).  This gets used in -U mode, or for
                                "log" rules since then we need all of the
                                packet to be logged (via the ULOG pcap
                                writer).
    --dump-conf               - Dump configuration on STDOUT and exit.
    --debug                   - Run in debug mode.
    -v   --verbose            - Run in verbose mode.
    -V   --Version            - Print fwsnort version number and exit.
    -h   --help               - Display usage on STDOUT and exit.

_USAGE_
    exit $exit;
}
