#!/usr/bin/perl

# Copyright 2003 Neil Gorsuch
#
# This file is part of pfilter.
#
# pfilter is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# pfilter 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

#=======================================================================
#
#       PERL SETUP

use strict;

$| = 1;

#=======================================================================
#
#       SYSTEM PROGRAM LOCATIONS (they are also searched for on the PATH)

# presumed location of ifconfig program
my $ifconfig_default_path = "/sbin/ifconfig";

# presumed locations of ipchains and iptables programs
my $ipchains_default_path = "/sbin/ipchains";
my $iptables_default_path = "/sbin/iptables";

# location of route program
my $route_default_path = "/sbin/route";

#=======================================================================
#
#       LOCATION OF PARSED SYSTEM FILES

# protocols file
my $protocols_path = "/etc/protocols";

# services file
my $services_path = "/etc/services";

#=======================================================================
#
#       LOAD IN LIBRARIES AND OTHER INCLUDED PERL SOURCE FILES

my @included_files = (
		      "Config.pl",
		      "Expand.pl",
		      "Generate.pl",
		      "Interfaces.pl",
		      "Lib.pl",
		      "Mining.pl",
		      "Parse.pl",
		      "Protocols.pl",
		      "Rulesets.pl",
		      "Services.pl",
		      "Subs.pl"
		      );

foreach my $libname ( @included_files ) {
    if ( -f "Pfilter/$libname" ) {
#	print "using Pfilter/$libname\n";
	require "Pfilter/$libname";
    } elsif ( -f "/usr/lib/pfilter/$libname" ) {
#	print "using /usr/lib/pfilter/$libname\n";
	require "/usr/lib/pfilter/$libname";
    } else {
	die "cannot find library $libname at either Pfilter or /usr/lib/pfilter\n";
    }
}

#=======================================================================
#
#       SET DEFAULT VARIABLE VALUES AND LIST VALUES

my $vars_ref = init_vars();

# fill in stuff modified by "make pfilter" or "make tar" or "make rpms"

${$$vars_ref{constants}}{config_file_path} = "/etc/pfilter.conf";
${$$vars_ref{constants}}{output_file_path} = "/etc/pfilter.cmds";
${$$vars_ref{constants}}{limits}           = 1;
${$$vars_ref{constants}}{pfilter_version } = "1.707";
${$$vars_ref{constants}}{source_file_path} = "/etc/pfilter.src";

# parsing/execution error lines without new-lines on end

my @error_lines;

# panic configuration file

my @panic_configuration = (
    "# DEFAULT CONFIGURATION FILE SUPPLIED BY PFILTER BECAUSE FILE",
    "# ${$$vars_ref{constants}}{config_file_path} COULD NOT BE OPENED",
    "untrusted  all",
    "open       ssh"
    );

# parsing/execution warning lines without new-lines on end

my @warning_lines = ();

#=======================================================================
#
# PARSE COMMAND LINE SWITCHES

# parse command line arguments

#print "ARGV = @ARGV \n";
foreach (@ARGV) {  # loop through command line switches
    #print "ARGV0 = $_\n";
    if (/^--config=(\S+)$/) {
	${$$vars_ref{constants}}{config_file_path} = $1;
    # we turn on both the overall and the later overriding 
    # command line stored debug flags in case we wanted 
    # debugging output during the config file compilation
    } elsif (/^(-d|--debug)$/) {
	${$$vars_ref{constants}}{debug} = 1;
	${$$vars_ref{constants}}{debug_cmdline} = 1;
    } elsif ($_ eq "--dumpconf") {
	${$$vars_ref{constants}}{dumpconf} = 1;
    } elsif ($_ eq "--dumpinterfaces") {
	${$$vars_ref{constants}}{dumpinterfaces} = 1;
    } elsif ($_ eq "--dumpservices") {
	${$$vars_ref{constants}}{dumpservices} = 1;
    } elsif (/^(-h|--help)$/) {
	usage( $vars_ref );
	exit 1;
    } elsif ($_ eq "--ipchains") {
	${$$vars_ref{constants}}{forced_mode} = "ipchains";
    } elsif ($_ eq "--iptables") {
	${$$vars_ref{constants}}{forced_mode} = "iptables";
    } elsif ($_ eq "--limits") {
	${$$vars_ref{constants}}{limits} = 1;
    } elsif ($_ eq "--logging") {
	${$$vars_ref{constants}}{logging_cmdline} = "limited";
    } elsif ($_ eq "--logging=none") {
	${$$vars_ref{constants}}{logging_cmdline} = "none";
    } elsif ($_ eq "--logging=limited") {
	${$$vars_ref{constants}}{logging_cmdline} = "limited";
    } elsif ($_ eq "--logging=full") {
	${$$vars_ref{constants}}{logging_cmdline} = "full";
    } elsif ($_ eq "--nocond") {
	${$$vars_ref{constants}}{nocond} = 1;
    } elsif ($_ eq "--noconst") {
	${$$vars_ref{constants}}{noconst} = 1;
    } elsif (/^(--nodebug)$/) {
	${$$vars_ref{constants}}{debug_cmdline} = 0;
    } elsif ($_ eq "--noexec") {
	${$$vars_ref{constants}}{exec} = 0;
    } elsif ($_ eq "--nolimits") {
	${$$vars_ref{constants}}{limits} = 0;
    } elsif ($_ eq "--nologging") {
	${$$vars_ref{constants}}{logging_cmdline} = "none";
    } elsif ($_ eq "--noloop") {
	${$$vars_ref{constants}}{noloop} = 1;
    } elsif ($_ eq "--nomacro") {
	${$$vars_ref{constants}}{nomacro} = 1;
    } elsif ($_ eq "--nooptpath") {
	${$$vars_ref{constants}}{nooptpath} = 1;
    } elsif ($_ eq "--noout") {
	${$$vars_ref{constants}}{noout} = 1;
    } elsif ($_ eq "--noprefix") {
	${$$vars_ref{constants}}{noprefix} = 1;
    } elsif ($_ eq "--nosuffix") {
	${$$vars_ref{constants}}{nosuffix} = 1;
    } elsif ($_ eq "--noout") {
	${$$vars_ref{constants}}{noout} = 1;
	${$$vars_ref{constants}}{exec} = 0;      # --noout implies --noexec
    } elsif ($_ eq "--noverbose") {
	${$$vars_ref{constants}}{verbose_cmdline} = 0;
    } elsif (/^(--nowarnings)$/) {
	$^W = 0;
	${$$vars_ref{constants}}{warnings} = 0;
    } elsif (/^--output=(\S+)$/) {
	${$$vars_ref{constants}}{output_file_path} = $1;
        if ( ${$$vars_ref{constants}}{output_file_path} eq "-" ) {
            print "$0: output file ${$$vars_ref{constants}}{output_file_path} is stdout, disabling execution\n"
	        if ${$$vars_ref{constants}}{debug};
            ${$$vars_ref{constants}}{exec} = 0;
        }
    } elsif (/^--source=(\S+)$/) {
	${$$vars_ref{constants}}{source_file_path} = $1;
        if ( ${$$vars_ref{constants}}{source_file_path} eq "-" ) {
            print "$0: source file ${$$vars_ref{constants}}{source_file_path} is stdout, disabling output file generation and disabling execution\n"
	        if ${$$vars_ref{constants}}{debug};
	    ${$$vars_ref{constants}}{noout} = 1;
            ${$$vars_ref{constants}}{exec} = 0;
        }
    } elsif (/^(-v|--verbose)$/) {
	${$$vars_ref{constants}}{verbose_cmdline} = 1;
    } elsif ($_ eq "--version") {
	print "$0: version ${$$vars_ref{constants}}{pfilter_version}\n";
	exit 1;
    } elsif (/^(-w|--warnings)$/) {
	$^W = 1;
	${$$vars_ref{constants}}{warnings} = 1;
    } elsif (/^(restart|start|status|chains|stop)$/) {
        # If the command is "status" or "chains", and we already have
	# a generated pfilter output commands file from before, just
	# invoke the commands file to save time. This will skip 
	# parsing the configuration file, parsing the ruleset files,
	# and generating the commands file.
	${$$vars_ref{constants}}{action} = $_;
        my $action = ${$$vars_ref{constants}}{action};
        if ( ( $action eq "status" || $action  eq "chains" ) &&
	     ${$$vars_ref{constants}}{output_file_path} ne "-" &&
	     -x ${$$vars_ref{constants}}{output_file_path} ) {
            print "$0: executing ${$$vars_ref{constants}}{output_file_path} $action\n"
                if ${$$vars_ref{constants}}{debug};
            my $status = system "${$$vars_ref{constants}}{output_file_path} $action";
            exit $status;            
        }
    } else {
	print "$0: unknown command line switch \<$_\>\n";
	usage( $vars_ref );
	exit 1;
    }
}

#=======================================================================
#
# LOOK FOR REQUIRED SYSTEM EXECUTABLES

${$$vars_ref{defines}}{ifconfig_path} = 
    required_executable($ifconfig_default_path);
if ( ${$$vars_ref{constants}}{debug} ) {
    if ( ${$$vars_ref{defines}}{ifconfig_path} ) {
	print "$0: looking for $ifconfig_default_path ... found at ${$$vars_ref{defines}}{ifconfig_path}\n";
    } else {
	print "$0: looking for $ifconfig_default_path ... no executable found\n";
    }
}

${$$vars_ref{defines}}{route_path} = 
    required_executable($route_default_path);
if ( ${$$vars_ref{constants}}{debug} ) {
    if ( ${$$vars_ref{defines}}{route_path} ) {
	print "$0: looking for $route_default_path ... found at ${$$vars_ref{defines}}{route_path}\n";
    } else {
	print "$0: looking for $route_default_path ... no executable found\n";
    }
}

my $iptables_path = find_executable($iptables_default_path);
${$$vars_ref{defines}}{iptables_path} = $iptables_path
    if $iptables_path;
if ( ${$$vars_ref{constants}}{debug} ) {
    if ( ${$$vars_ref{defines}}{iptables_path} ) {
	print "$0: looking for $iptables_default_path ... found at ${$$vars_ref{defines}}{iptables_path}\n";
    } else {
	print "$0: looking for $iptables_default_path ... no executable found\n";
    }
}

my $ipchains_path = find_executable($ipchains_default_path);
${$$vars_ref{defines}}{ipchains_path} = $ipchains_path
    if $ipchains_path;
if ( ${$$vars_ref{constants}}{debug} ) {
    if ( $ipchains_path ) {
	print "$0: looking for $ipchains_default_path ... found at ${$$vars_ref{defines}}{ipchains_path}\n";
    } else {
	print "$0: looking for $ipchains_default_path ... no executable found\n";
    }
}

if ( ! -x ${$$vars_ref{defines}}{ipchains_path} && ! -x ${$$vars_ref{defines}}{iptables_path} ) {
    print "Warning, no executable iptables or ipchains programs found at default\n";
    print "locations of $iptables_default_path or $ipchains_default_path or on PATH\n";
}

#=======================================================================
#
# PARSE /etc/protocols FILE

$$vars_ref{protocols} = 
    parse_etc_protocols( $vars_ref, $protocols_path );

#=======================================================================
#
# PARSE /etc/services FILE and find named services/protocols/ports

$$vars_ref{named_services_protocols_ports} = 
    parse_etc_services( $vars_ref, $services_path );

#=======================================================================
#
# FIND THE LIST OF INTERFACE NAMES AND THE ADDRESS AND BROADCAST ADDRESS
# AND ADDRESS MASK OF EACH AND FIGURE OUT WHICH HAVE RFC1918 ADDRESSES,
# AND FIND THE LIST OF INTERFACE NAMES THAT ARE DEFAULT ROUTE INTERFACES
# (there should only be one, but mistakes happen) AND MARK THE ONES
# THAT ARE DEFAULT ROUTES

sleep 2;  # Give enough time for the service Network to initialize
my $interfaces_ref = find_interfaces( $vars_ref );    
my @interface_names = sort keys %{$interfaces_ref};
$$vars_ref{interfaces} = $interfaces_ref;
$$vars_ref{interface_names} = \@interface_names;
warn "$0: cannot find any interface names with ${$$vars_ref{defines}}{ifconfig_path} command\n"
    if ! %{$$vars_ref{interfaces}} && ${$$vars_ref{constants}}{exec};
my %remaining_interfaces = %{$$vars_ref{interfaces}};

# store %constant definition for all interface names
${$$vars_ref{constants}}{all_interfaces} = join( " ", @interface_names );

# find and store the internal pointer to the list of interface
# addresses and broadcast addresses and the %constant definition
# for all interface addresses and broadcast addresses
my @interface_addresses = ();
my @broadcast_addresses = ();
foreach my $interface_name ( @interface_names ) {
    my $interface_ref = $$interfaces_ref{$interface_name};
    push @interface_addresses, $$interface_ref{address}
        if exists $$interface_ref{address};
    push @broadcast_addresses, $$interface_ref{broadcast}
        if exists $$interface_ref{broadcast};
}
my @surmised_broadcast_addresses = ( "255.255.255.255" );
foreach my $address ( @broadcast_addresses, @interface_addresses ) {
    my $surmised = $address;
    $surmised =~ s,\.[0-9]+$,.255,;
    push @surmised_broadcast_addresses, $surmised;
    $surmised =~ s,\.[0-9]+\.[0-9]+$,.255.255,;
#   push @surmised_broadcast_addresses, $surmised;
#   $surmised =~ s,\.[0-9]+\.[0-9]+\.[0-9]+$,.255.255.255,;
    push @surmised_broadcast_addresses, $surmised;
}
foreach my $surmised_broadcast ( @surmised_broadcast_addresses) {
    push @broadcast_addresses, $surmised_broadcast
	if ! grep( /^$surmised_broadcast$/, @broadcast_addresses );
}
$$vars_ref{interface_addresses} = \@interface_addresses;
${$$vars_ref{constants}}{interface_addresses} = 
    join( " ", @interface_addresses );
$$vars_ref{broadcast_addresses} = \@broadcast_addresses;
${$$vars_ref{constants}}{broadcast_addresses} = 
    join( " ", @broadcast_addresses );

my @default_route_interfaces = 
    default_route_interfaces( $vars_ref,
			      \@error_lines,
			      \@warning_lines );
$$vars_ref{default_route_interfaces} = \@default_route_interfaces;
${$$vars_ref{constants}}{default_route_interfaces} = 
    join( " ", @default_route_interfaces );
print "$0: default_route_interfaces=<@{$$vars_ref{default_route_interfaces}}>\n" 
    if ${$$vars_ref{constants}}{debug};
foreach my $default_route_interface_name ( @interface_names ) {
    my $interface_ref = 
	$$interfaces_ref{$default_route_interface_name};
    $$interface_ref{default_route} =
	( grep( /^$default_route_interface_name$/, @default_route_interfaces ) ) ?
	    1 : 0;
}

if ( ${$$vars_ref{constants}}{debug} ) {
    print "$0: found interfaces <", join(" ", @{$$vars_ref{interface_names}}), ">\n";
    print_hash("", "interfaces", $$vars_ref{interfaces} );
}

#=======================================================================
#
# FIND THE DIRECTORY THAT HAS THE RULESETS FILES

my $rulesets_directory;
if ( -f "rulesets/pfilter.default.rulesets" ) {
    print "$0: using directory rulesets for rulesets\n" if ${$$vars_ref{constants}}{debug};
    $rulesets_directory = "rulesets";
} elsif ( -f "/etc/pfilter.rulesets/pfilter.default.rulesets" ) {
    print "$0: using directory /etc/pfilter.rulesets for rulesets\n" if ${$$vars_ref{constants}}{debug};
    $rulesets_directory = "/etc/pfilter.rulesets";
} else {
    die "$0: cannot find pfilter rulesets directory at either rulesets or /etc/pfilter.rulesets\n";
}

#=======================================================================
#
# PARSE RULESETS FILES

# parse the files

parse_ruleset_files( $vars_ref,
		     $rulesets_directory,
		     "pfilter.default.rulesets",
		     \@error_lines,
		     \@warning_lines );
#    print "error_lines=@error_lines warning_lines=@warning_lines\n";

# output debug stuff if needed

#if ( ${$$vars_ref{constants}}{debug} ) {
#    if ( %{$$vars_ref{macros}} )
#	print_hash( "", "after parse_rulesets macros", $$vars_ref{macros} );
#    if ( %{$$vars_ref{constants}} )
#	print_hash( "", "after parse_rulesets constants", $$vars_ref{constants} );
#    if ( %{$$vars_ref{defines}} )
#	print_hash( "", "after parse_rulesets defines", $$vars_ref{defines} );
#}

# make sure certain macros have been defined

my @required_macros = (
		       "close_protocol_port",
		       "open_protocol_port",
		       "pfilter_alias",
		       "pfilter_prefix",
		       "pfilter_suffix",
		       "service_default_open"
		       );
my $macros_ref = $$vars_ref{macros};
foreach my $macro_name ( @required_macros ) {
    die "$0: required macro $macro_name (normally defined in $rulesets_directory/pfilter.default.rulesets) has not been defined\n"
	if ! exists $$macros_ref{$macro_name};
}

# mine network services from the rulesets

mine_network_services( $vars_ref, \@error_lines, \@warning_lines );

#=======================================================================
#
# READ IN THE CONFIGURATION FILE

# read in configuration file lines

# open config file for reading
my @conf_lines;
if ( ! open(CONF, ${$$vars_ref{constants}}{config_file_path}) ) {
    warn "$0: error, cannot open configuration file " .
    "${$$vars_ref{constants}}{config_file_path},\n" .
    "using instead the following default configuration:\n";
    foreach my $line ( @ panic_configuration ) {
	warn "$line\n";
	push @conf_lines, $line;
    }
} else {
    @conf_lines = <CONF>; # slurp in configuration file lines
    close(CONF);
    chomp @conf_lines;       # lop off new-line characters at end of lines
}

#=======================================================================
#
# scan configuration file for command line argument equivalent directives
# that we need to act on immediately before we do the full parsing of the
# configuration file

${$$vars_ref{constants}}{debug} = 1
    if ( grep ( /^\s*DEBUG(\s*$|\s*\#)/i, ( @conf_lines ) ) );
${$$vars_ref{constants}}{verbose} = 1
    if ( grep ( /^\s*VERBOSE(\s*$|\s*\#)/i, ( @conf_lines ) ) );
print "$0: turning on verbose comments\n" if ${$$vars_ref{constants}}{verbose};
if (${$$vars_ref{constants}}{debug}) {
    print "$0: turning on debug output\n";
    print "$0: conf=${$$vars_ref{constants}}{config_file_path}\n";
    print "$0: read ", scalar @conf_lines, " lines from ${$$vars_ref{constants}}{config_file_path}\n";
}

#=======================================================================
#
# PARSE CONSTANTS AND DEFINES, AND EXPAND MACROS AND CONDITIONALS AND
# METADIRECTIVES, FROM THE CONFIGURATION FILE

expand_lines( $vars_ref,
	      \@conf_lines,
	      \@error_lines,
	      \@warning_lines );

#=======================================================================
#
# PARSE COMMANDS FROM CONFIGURATION FILE

# parse the file

my ( $directives_ref ) = parse_pfilter_conf_lines ( $vars_ref,
						    \@conf_lines,
						    \@error_lines,
						    \@warning_lines );
$$vars_ref{config} = $directives_ref;

# output debug stuff if needed

if ( ${$$vars_ref{constants}}{debug} || ${$$vars_ref{constants}}{dumpconf} ) {
    if ( @$directives_ref ) {
	print "$0: parsed config directives ->\n";
	foreach my $hash_ref ( @{$$vars_ref{config}} ) {
	    if ( $$hash_ref{directive_type} eq "comment_lines" ) {
		print "$0:     comment ( $$hash_ref{comment_lines_num} lines at $$hash_ref{comment_lines_ref})\n";
	    } else {
		print_hash( "    ", "directive", $hash_ref );
	    }
	}
    }
}
if ( ${$$vars_ref{constants}}{debug} || ${$$vars_ref{constants}}{dumpservices} ) {
    my $services_ref = $$vars_ref{services};
    if ( %$services_ref ) {
	print_hash( "  ", "parsed text including network services",
		    $services_ref );
    }
}

#=======================================================================
#
# IF THERE WERE COMMAND LINE OPTIONS THAT OVERRIDE CONFIGURATION FILE
# OPTIONS, USE THE COMMENT LINE OPTIONS INSTEAD

${$$vars_ref{constants}}{debug} = ${$$vars_ref{constants}}{debug_cmdline}
    if exists ${$$vars_ref{constants}}{debug_cmdline};

${$$vars_ref{constants}}{logging} = ${$$vars_ref{constants}}{logging_cmdline}
    if exists ${$$vars_ref{constants}}{logging_cmdline};

${$$vars_ref{constants}}{verbose} = ${$$vars_ref{constants}}{verbose_cmdline}
    if exists ${$$vars_ref{constants}}{verbose_cmdline};

#=======================================================================
#
# PROCESS PARSED CONFIG FILE AND BUILD OUTPUT COMMANDS TEXT THAT WILL
# THEN BE CONVERTED TO A SHELL OUTPUT COMMANDS FILE USING MACROS

# start out by scanning through the parsed output for directives that
# affect which network interfaces are filtered etc., and set the
# interface attributes according to the directives
# for each directive ...
foreach my $directive_ref ( @{$$vars_ref{config}} ) {

    # if it is a directive that modifies interface attributes ...
    my $directive_type = $$directive_ref{directive_type};
    if ( $directive_type eq "extif"       ||
	 $directive_type eq "filtered"    ||
	 $directive_type eq "intif"       ||
	 $directive_type eq "private"     ||
	 $directive_type eq "protected"   ||
	 $directive_type eq "public"      ||
	 $directive_type eq "trusted"     ||
	 $directive_type eq "unfiltered"  ||
	 $directive_type eq "unprotected" ||
	 $directive_type eq "untrusted" ) {

	# for each interface modified by the directive ...
	my $interface_names_ref = $$directive_ref{interfaces};
	if ( grep ( /^ALL$/, @$interface_names_ref ) ) {
	    my $all_interfaces = ${$$vars_ref{constants}}{all_interfaces};
	    my @all_interfaces = split( /\s/, $all_interfaces);
	    $interface_names_ref = \@all_interfaces;
        }
	my $byonin = "by $directive_type directive"
	    . " on line $$directive_ref{config_line_number}"
		. " in ${$$vars_ref{constants}}{config_file_path}";
	foreach my $interface_name ( @$interface_names_ref ) {
	    
	    # check if it is a current network interface, and 
	    # if not, output a warning message
	    $interfaces_ref = $$vars_ref{interfaces};
	    if ( ! exists $$interfaces_ref{$interface_name} && ${$$vars_ref{constants}}{warnings} ) {
                print "$0: warning - invalid interface $interface_name for $directive_type on line $$directive_ref{config_line_number} in ${$$vars_ref{constants}}{config_file_path}\n";
                print "$0: Hopefully, this is a dynamic interface and will appear later\n";
	    }

	    # modify the interface structure according to the directive
            # EXTIF
	    if ( $directive_type eq "extif" ) { # EXTIF is FILTERED UNPROTECTED
		# set interface FILTERED and UNPROTECTED
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_filtered', "FILTERED", 
		      'declared_unfiltered', "UNFILTERED",
		      $byonin );
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unprotected', "UNPROTECTED", 
		      'declared_protected', "PROTECTED",
		      $byonin );
            # FILTERED
	    } elsif ( $directive_type eq "filtered" ) {
		# set interface FILTERED (also known as UNTRUSTED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_filtered', "FILTERED", 
		      'declared_unfiltered', "UNFILTERED",
		      $byonin );
            # INTIF
	    } elsif ( $directive_type eq "intif" ) { # INTIF is UNFILTERED PROTECTED
		# set interface UNFILTERED and PROTECTED
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unfiltered', "UNFILTERED", 
		      'declared_filtered', "FILTERED",
		      $byonin );
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_protected', "PROTECTED", 
		      'declared_unprotected', "UNPROTECTED",
		      $byonin );
            # PRIVATE
	    } elsif ( $directive_type eq "private" ) {
		# set interface PRIVATE (other name for PROTECTED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_protected', "PROTECTED", 
		      'declared_unprotected', "UNPROTECTED",
		      $byonin );
            # PROTECTED
	    } elsif ( $directive_type eq "protected" ) {
		# set interface PROTECTED (also known as PRIVATE)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_protected', "PROTECTED", 
		      'declared_unprotected', "UNPROTECTED",
		      $byonin );
            # PUBLIC
	    } elsif ( $directive_type eq "public" ) {
		# set interface PUBLIC (other name for UNPROTECTED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unprotected', "UNPROTECTED", 
		      'declared_protected', "PROTECTED",
		      $byonin );
	    # TRUSTED
	    } elsif ( $directive_type eq "trusted" ) {
		# set interface TRUSTED (other name for UNFILTERED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unfiltered', "UNFILTERED", 
		      'declared_filtered', "FILTERED",
		      $byonin );
	    # UNFILTERED
	    } elsif ( $directive_type eq "unfiltered" ) {
		# set interface UNFILTERED (also known as TRUSTED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unfiltered', "UNFILTERED", 
		      'declared_filtered', "FILTERED",
		      $byonin );
            # UNPROTECTED
	    } elsif ( $directive_type eq "unprotected" ) {
		# set interface UNPROTECTED
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_unprotected', "UNPROTECTED", 
		      'declared_protected', "PROTECTED",
		      $byonin );
            # UNTRUSTED
	    } elsif ( $directive_type eq "untrusted" ) {
		# set interface UNTRUSTED (other name for FILTERED)
		interface_set_check_conflicting_flags
		    ( $vars_ref, $$interfaces_ref{$interface_name},
		      'declared_filtered', "FILTERED", 
		      'declared_unfiltered', "UNFILTERED",
		      $byonin );
	    }
	}

	# mark this stored directive as being processed
	# (this is used later to watch for coding errors
	# that leave directives unprocessed when 
	# generating the ingternediate output source file)
	$$directive_ref{processed} = 1;

    }
}
print_hash("", "after interface directives interfaces", 
	   $$vars_ref{interfaces} )
    if ${$$vars_ref{constants}}{debug};

# loop through the directives, searching for DROP or REJECT directives
foreach my $directive_ref ( @{$$vars_ref{config}} ) {
    my $directive_type = $$directive_ref{directive_type};
    if ( $directive_type eq "drop" ) {
	${$$vars_ref{defines}}{nowayit} = "DROP";
	${$$vars_ref{defines}}{nowayed} = "DROPPED";
	$$directive_ref{processed} = 1;
    } elsif ( $directive_type eq "reject" ) {
	${$$vars_ref{defines}}{nowayit} = "REJECT";
	${$$vars_ref{defines}}{nowayed} = "REJECTED";
	$$directive_ref{processed} = 1;
    }
}

# loop through all the interfaces, and for any unfiltered/filtered or
# unprotected/protected attributes that have not been set yet, set them
# according to whether or not the interface has an rfc1918 address and
# whether it has a default route marked as going out on it
# (except if one or more of the interfaces have been explicitly declared
# to be PROTECTED, set all remaining undeclared interfaces as UNPROTECTED.

# find out if any interfaces have been declared as PROTECTED
my $protected_interfaces_flag = 0;
$interfaces_ref = $$vars_ref{interfaces};
foreach my $interface_name ( keys %$interfaces_ref ) {
    my $interface_ref = $$interfaces_ref{$interface_name};
    if ( $$interface_ref{declared_protected} ) {
	$protected_interfaces_flag = 1;
	last;
    }
}
# for each interface ...
$interfaces_ref = $$vars_ref{interfaces};
foreach my $interface_name ( keys %$interfaces_ref ) {
    my $interface_ref = $$interfaces_ref{$interface_name};

    # if unfiltered/filtered set, honor that
    if ( $$interface_ref{declared_unfiltered} ) {
	$$interface_ref{filtered} = 0;
    } elsif ( $$interface_ref{declared_filtered} ) {
	$$interface_ref{filtered} = 1;
    }
    # if unfiltered/filtered has not been set either way, ...
    elsif ( ! $$interface_ref{declared_unfiltered} && ! $$interface_ref{declared_filtered} ) {
	# set to filtered no matter what
	$$interface_ref{filtered} = 1;
	push @warning_lines, "interface $$interface_ref{name} with address address $$interface_ref{address} set to FILTERED because not set otherwise";
    }

    # if unprotected/protected set, honor that
    if ( $$interface_ref{declared_unprotected} ) {
	$$interface_ref{protected} = 0;
    } elsif ( $$interface_ref{declared_protected} ) {
	$$interface_ref{protected} = 1;
    }
    # if protected/unprotected has not been set either way, ...
    elsif ( ! $$interface_ref{declared_protected} && ! $$interface_ref{declared_unprotected} ) {
	# set to unprotected if one or more interfaces
	# were declared to be protected, otherwise, 
	# set protected if rfc1918 address and not a default route,
	# unprotected if not
	if ( $protected_interfaces_flag ) {
	    $$interface_ref{protected} = 0;
	    push @warning_lines, "interface $$interface_ref{name} with address $$interface_ref{address} set to UNPROTECTED because because other interface(s) were declared to be PROTECTED";
	} elsif ( $$interface_ref{rfc1918} ) {
	    if ( $$interface_ref{default_route} ) {
		$$interface_ref{protected} = 0;
		push @warning_lines, "interface $$interface_ref{name} set to UNPROTECTED because it is marked as a default route";
	    } else {
		$$interface_ref{protected} = 1;
		push @warning_lines, "interface $$interface_ref{name} set to PROTECTED because of rfc1918 address $$interface_ref{address}";
	    }
	} else {
	    $$interface_ref{protected} = 0;
	    push @warning_lines, "interface $$interface_ref{name} set to UNPROTECTED because of non-rfc1918 address $$interface_ref{address}";
	}
    }
}

print_hash("", "after interface directives and applied defaults interfaces", 
	   $$vars_ref{interfaces} )
    if ${$$vars_ref{constants}}{debug};

# store %constant definitions for interface name groups
# by looping through all the network interfaces and 
# building lists
my @filtered_interface_addresses = ();
my @filtered_interface_names = ();
@interface_names = ();
my @protected_interface_addresses = ();
my @protected_interface_names = ();
my @unfiltered_interface_addresses = ();
my @unfiltered_interface_names = ();
my @unprotected_interface_addresses = ();
my @unprotected_interface_names = ();
$interfaces_ref = $$vars_ref{interfaces};
foreach my $interface_name ( keys %$interfaces_ref ) {
    my $interface_ref = $$interfaces_ref{$interface_name};
    push @interface_names,$interface_name;
    if ( $$interface_ref{filtered} ) {
	push @filtered_interface_names,$interface_name;
	push @filtered_interface_addresses,$$interface_ref{address}
	    if exists $$interface_ref{address};
    } else {
	push @unfiltered_interface_names,$interface_name;
	push @unfiltered_interface_addresses,$$interface_ref{address}
	    if exists $$interface_ref{address};
    }
    if ( $$interface_ref{protected} ) {
	push @protected_interface_names,$interface_name;
	push @protected_interface_addresses,$$interface_ref{address}
	    if exists $$interface_ref{address};
    } else {
	push @unprotected_interface_names,$interface_name;
	push @unprotected_interface_addresses,$$interface_ref{address}
	    if exists $$interface_ref{address};
    }
}


$$vars_ref{filtered_interface_addresses} = \@filtered_interface_addresses;
${$$vars_ref{constants}}{filtered_interface_addresses} = 
    join( " ", @filtered_interface_addresses );

$$vars_ref{filtered_interface_names} = \@filtered_interface_names;
${$$vars_ref{constants}}{filtered_interface_names} = 
    join( " ", @filtered_interface_names );

$$vars_ref{interface_names} = \@interface_names;
${$$vars_ref{constants}}{interface_names} = 
    join( " ", @interface_names );

$$vars_ref{protected_interface_addresses} = \@protected_interface_addresses;
${$$vars_ref{constants}}{protected_interface_addresses} = 
    join( " ", @protected_interface_addresses );

$$vars_ref{protected_interface_names} = \@protected_interface_names;
${$$vars_ref{constants}}{protected_interface_names} =
    join( " ", @protected_interface_names );

$$vars_ref{unfiltered_interface_addresses} = \@unfiltered_interface_addresses;
${$$vars_ref{constants}}{unfiltered_interface_addresses} = 
    join( " ", @unfiltered_interface_addresses );

$$vars_ref{unfiltered_interface_names} = \@unfiltered_interface_names;
${$$vars_ref{constants}}{unfiltered_interface_names} = 
    join( " ", @unfiltered_interface_names );

$$vars_ref{unprotected_interface_addresses} = \@unprotected_interface_addresses;
${$$vars_ref{constants}}{unprotected_interface_addresses} = 
    join( " ", @unprotected_interface_addresses );

$$vars_ref{unprotected_interface_names} = \@unprotected_interface_names;
${$$vars_ref{constants}}{unprotected_interface_names} =
    join( " ", @unprotected_interface_names );

if ( ${$$vars_ref{constants}}{debug} || ${$$vars_ref{constants}}{dumpinterfaces} ) {
    my $interfaces_ref = $$vars_ref{interfaces};
    if ( %$interfaces_ref ) {
	print_hash( "  ", "determined interfaces", $interfaces_ref );
    }
}
print_hash("", "after interface munging constants", $$vars_ref{constants})
    if ${$$vars_ref{constants}}{debug};

# Store a %constant value ${$$vars_ref{constants}}{alias_faked_addresses}
# and store an array pointed to by $$vars_ref{alias_faked_addresses}
# for all the alias faked addresses.
# Store a %constant value ${$$vars_ref{constants}}{alias_real_addresses}
# and store an array pointed to by $$vars_ref{alias_real_addresses}
# for all the alias real addresses.
# Store a hash pointed to by $$vars_ref{alias_real_to_faked} that
# has faked address keys pointing to corresponding real addresses.
# Store a hash pointed to by $$vars_ref{alias_faked_to_real} that
# has real address keys pointing to corresponding faked addresses.

my @alias_faked_addresses;
my %alias_faked_to_real;
my @alias_real_addresses;
my %alias_real_to_faked;

foreach my $directive_ref ( @{$$vars_ref{config}} ) {
    my $directive_type = $$directive_ref{directive_type};
    if ( $directive_type eq "alias" ) {
	push @alias_faked_addresses, $$directive_ref{faked_address};
	$alias_faked_to_real{$$directive_ref{faked_address}} =
	    $$directive_ref{real_address};
	push @alias_real_addresses, $$directive_ref{real_address};
	$alias_real_to_faked{$$directive_ref{real_address}} =
	    $$directive_ref{faked_address};
    }
}
$$vars_ref{alias_faked_addresses} = \@alias_faked_addresses;
${$$vars_ref{constants}}{alias_faked_addresses} = 
    join( " ", @alias_faked_addresses );
$$vars_ref{alias_faked_to_real} = \%alias_faked_to_real;
$$vars_ref{alias_real_addresses} = \@alias_real_addresses;
${$$vars_ref{constants}}{alias_real_addresses} = 
    join( " ", @alias_real_addresses );
$$vars_ref{alias_real_to_faked} = \%alias_real_to_faked;

#=======================================================================
#
# SEE WHICH KERNEL NAME AND VERSION WE HAVE AND IF IT CAN SUPPORT IPTABLES

my ($kernel_name) = find_kernel_name();
my ($kernel_major, $kernel_minor) = find_kernel_version();
print "$0: kernel_name=$kernel_name kernel_major=$kernel_major kernel_minor=$kernel_minor\n"
    if ${$$vars_ref{constants}}{debug};
my $iptables_kernel = $kernel_name eq "Linux" &&
    ( $kernel_major > 2 ||
      ( $kernel_major == 2 && $kernel_minor >= 4 ) );
print "$0: iptables_kernel = $iptables_kernel\n" if ${$$vars_ref{constants}}{debug};

#===========================================================================
#
# FIGURE OUT WHETHER WE USE IPTABLES OR IPCHAINS, PREFERRING IPTABLES IF POSSIBLE

# if they wanted forced iptables mode ...
if ( ${$$vars_ref{constants}}{forced_mode} eq "iptables" ) {
    # but if the kernel on this machine does not support iptables ...
    if ( ! $iptables_kernel ) {
	print "The kernel on this machine is too old to support iptables.\n";
	# if they are not in --noexec or --noout modes force it to ipchains
	if ( ${$$vars_ref{constants}}{exec} ) {
	    print "Over-riding --iptables switch, forcing into ipchains mode.\n";
	    ${$$vars_ref{constants}}{mode} = "ipchains";
	} else {
	    print "Writing non-executed commands file in iptables mode.\n";
	    print "This commands file will not be executable on this machine!\n";
	    ${$$vars_ref{constants}}{mode} = "iptables";
	}
    # if the kernel is recent enough to support iptables let them use it
    } else {
	print "$0: setting forced iptables mode\n" if ${$$vars_ref{constants}}{debug};
	${$$vars_ref{constants}}{mode} = "iptables";
    }
# if they wanted forced ipchains mode ...
} elsif ( ${$$vars_ref{constants}}{forced_mode} eq "ipchains" ) {
    print "$0: setting forced ipchains mode\n" if ${$$vars_ref{constants}}{debug};
    ${$$vars_ref{constants}}{mode} = "ipchains";
# if no execution specified, use iptables if kernel is recent enough to support it
} elsif ( ! ${$$vars_ref{constants}}{exec} ) {
    if ( $iptables_kernel ) {
	print "$0: kernel recent enough linux, setting iptables mode\n" if ${$$vars_ref{constants}}{debug};
	${$$vars_ref{constants}}{mode} = "iptables";
    } else {
	print "$0: kernel too old, setting ipchains mode\n" if ${$$vars_ref{constants}}{debug};
	${$$vars_ref{constants}}{mode} = "ipchains";
    }
# no forced mode, execution wanted, use iptables if kernel recent enough and
# iptables works (should try to remove ipchains module and insert iptables
# kernel module if no filtering rules in effect but I will add that later
} else {
    if ( $iptables_kernel ) {
	if ( -x ${$$vars_ref{defines}}{iptables_path} ) {
	    my $command = "${$$vars_ref{defines}}{iptables_path} -L -n >/dev/null 2>/dev/null";
	    print "$0: testing \"$command\" command\n"
		if ${$$vars_ref{constants}}{debug};
	    if ( ! system "$command" ) {
		print "$0: ${$$vars_ref{defines}}{iptables_path} is usable\n" if ${$$vars_ref{constants}}{debug};
		${$$vars_ref{constants}}{mode} = "iptables";
            } elsif ( -x ${$$vars_ref{defines}}{iptables_path} ) {
		print "${$$vars_ref{defines}}{iptables_path} is executable but not working yet.\n";
		print "Perhaps you need to rmmod ipchains and insmod iptables?\n";
		print "(pfilter will do this for you when it starts, so you can probably ignore this)\n";
		${$$vars_ref{constants}}{mode} = "iptables";
	    } else {
		print "${$$vars_ref{defines}}{iptables_path} is not usable, setting ipchains mode.\n";
		print "Perhaps you need to rmmod ipchains and insmod iptables?\n";
		${$$vars_ref{constants}}{mode} = "ipchains";
	    }
	} else {
	    print "$0: iptables is not working, setting ipchains mode\n" if ${$$vars_ref{constants}}{debug};
	    ${$$vars_ref{constants}}{mode} = "ipchains";
	}
    } else {
	if ( -x ${$$vars_ref{defines}}{ipchains_path} ) {
	    print "$0: testing \"${$$vars_ref{defines}}{ipchains_path} -L 2>/dev/null\" command\n"
		if ${$$vars_ref{constants}}{debug};
	    my @ipchains_output = `${$$vars_ref{defines}}{ipchains_path} -L 2>/dev/null`;
	    if ( @ipchains_output ) {
		print "$0: ${$$vars_ref{defines}}{ipchains_path} is usable setting ipchains mode\n"
		    if ${$$vars_ref{constants}}{debug};
		${$$vars_ref{constants}}{mode} = "ipchains";
	    } else {
		print "$0: ${$$vars_ref{defines}}{ipchains_path} is not usable but no choice setting ipchains mode\n"
		    if ${$$vars_ref{constants}}{debug};
		${$$vars_ref{constants}}{mode} = "ipchains";
	    }
	}
    }
}

# now that we have decided whether to use ipchains or iptables,
# make sure we know the path to the appropriate binary
if ( ${$$vars_ref{constants}}{mode} eq "iptables" ) {
    unless ( -x ${$$vars_ref{defines}}{iptables_path} ) {
	print "No executable iptables at standard location ${$$vars_ref{defines}}{iptables_path}.\n";
	print "Hopefully, iptables is in the PATH\n";
	${$$vars_ref{defines}}{iptables_path} =~ s,^.*/,,;
    }
} elsif ( ${$$vars_ref{constants}}{mode} eq "ipchains" ) {
    unless ( -x ${$$vars_ref{defines}}{ipchains_path} ) {
	print "No executable ipchains at standard location ${$$vars_ref{defines}}{ipchains_path}.\n";
	print "Hopefully, ipchains is in the PATH\n";
	${$$vars_ref{defines}}{ipchains_path} =~ s,^.*/,,;
    }
}

#=========================================================================
#
# generate the intermediate source file that will be run through the macro
# processor and then turned into the commands file

print_hash( "", "before generating the intermediate source file vars",
	    $vars_ref )
    if ${$$vars_ref{constants}}{debug};
print "$0: generating intermediate source file\n" 
    if ${$$vars_ref{constants}}{debug};
my @source_lines = ();
generate_source( $vars_ref,
		 \@source_lines,
		 \@error_lines,
		 \@warning_lines );

#=========================================================================
#
# write out the pre-expanded source file

if ( ${$$vars_ref{constants}}{debug} ) {
    my $lines = scalar(@source_lines);
    print "$0: writing $lines lines to pre-expanded source file ${$$vars_ref{constants}}{source_file_path}\n";
}
if ( ! open(SOURCE, "> ${$$vars_ref{constants}}{source_file_path}") ) {
    warn "$0: error, cannot write out pre-expanded source file ${$$vars_ref{constants}}{source_file_path}\n";
} else {
    foreach my $line ( @source_lines ) { print SOURCE "$line\n" };
    close(SOURCE);
    print "$0: chmod 700 ${$$vars_ref{constants}}{source_file_path}\n"
	if ${$$vars_ref{constants}}{debug};
    chmod 0700, ${$$vars_ref{constants}}{source_file_path};
}

#=========================================================================
#
# convert the generated source file into an output commands file,
# parsing constants and defines, and expanding macros and conditionals
# and metadirectives, and then write out the commands file

if ( ${$$vars_ref{constants}}{noout} ) {
    print "$0: skipping output commands file generation because of --noout command line option\n";
} else {
    my @output_lines = @source_lines;
    expand_lines( $vars_ref,
		  \@output_lines,
		  \@error_lines,
		  \@warning_lines );
    if ( ${$$vars_ref{constants}}{debug} ) {
        my $lines = scalar(@output_lines);
	print "$0: writing $lines lines to output file ${$$vars_ref{constants}}{output_file_path}\n";
    }
    if ( ! open(OUTPUT, "> ${$$vars_ref{constants}}{output_file_path}") ) {
        warn "$0: error, cannot write commands output file ${$$vars_ref{constants}}{output_file_path}\n";
    } else {
	foreach my $line ( @output_lines ) { print OUTPUT "$line\n" };
	close(OUTPUT);
	print "$0: chmod 700 ${$$vars_ref{constants}}{output_file_path}\n"
	    if ${$$vars_ref{constants}}{debug};
        chmod 0700, ${$$vars_ref{constants}}{output_file_path}
            unless ${$$vars_ref{constants}}{output_file_path} eq "-";
    }
}

#=========================================================================
#
# execute the output commands file

my $status = 0;
if ( ${$$vars_ref{constants}}{noout} ) {
    print "$0: skipping output commands file execution because of --noout command line option\n";
} elsif ( ! ( ${$$vars_ref{constants}}{exec} ) ) {
    print "$0: skipping output commands file execution because of --noexec command line option\n";
} else {
    print "$0: executing output file ${$$vars_ref{constants}}{output_file_path}\n"
	if ${$$vars_ref{constants}}{debug} && ${$$vars_ref{constants}}{exec};
    my $status = ( ${$$vars_ref{constants}}{exec} ) ?
        system "${$$vars_ref{constants}}{output_file_path} ${$$vars_ref{constants}}{action}" : 0;
}

#=========================================================================
#
# output accumulated warnings and errors

if ( ${$$vars_ref{constants}}{warnings} ) {
    foreach my $warning ( @warning_lines ) {
        chomp $warning;
        print "$0: warning - $warning\n";
    }
}
@warning_lines = ();
foreach my $error ( @error_lines ) {
    chomp $error;
    print "$0: error - $error\n";
}
@error_lines = ();

#=========================================================================
#
# that's all, folks, just syslog if appropropriate and then we're done

if (
    ${$$vars_ref{constants}}{action} eq "start" ||
    ${$$vars_ref{constants}}{action} eq "stop" ||
    ${$$vars_ref{constants}}{action} eq "restart"
    ) {
    my $logit = "logger -t pfilter " . ${$$vars_ref{constants}}{action} .
    ( ( ! $status ) ? " succeeeded" : " failed with status $status" );
    print "$0: $logit\n" if ${$$vars_ref{constants}}{debug};
    system "$logit";
}
exit $status;

