shorewall_code/New/Shorewall/Rules.pm

1504 lines
44 KiB
Perl
Raw Normal View History

#
# Shorewall 3.9 -- /usr/share/shorewall/Shorewall/Rules.pm
#
# This program is under GPL [http://www.gnu.org/copyleft/gpl.htm]
#
# (c) 2007 - Tom Eastep (teastep@shorewall.net)
#
# Complete documentation is available at http://shorewall.net
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of Version 2 of the GNU General Public License
# as published by the Free Software Foundation.
#
# 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., 675 Mass Ave, Cambridge, MA 02139, USA
#
#
package Shorewall::Rules;
require Exporter;
use Shorewall::Common;
use Shorewall::Config;
use Shorewall::Zones;
use Shorewall::Interfaces;
use Shorewall::Chains;
use Shorewall::Hosts;
use Shorewall::Actions;
use Shorewall::Macros;
use Shorewall::Policy;
use strict;
our @ISA = qw(Exporter);
our @EXPORT = qw( add_common_rules setup_mac_lists process_criticalhosts process_routestopped process_rules generate_matrix );
our @EXPORT_OK = qw( process_rule process_rule1 );
our @VERSION = 1.00;
#
# Set to one if we find a SECTION
#
my $sectioned = 0;
sub process_tos() {
my $chain = 'pretos';
my $stdchain = 'PREROUTING';
if ( -s "$ENV{TMP_DIR}/tos" ) {
progress_message2 'Setting up TOS...';
my $pretosref = new_chain 'mangle' , 'pretos';
my $outtosref = new_chain 'mangle' , 'outtos';
open TOS, "$ENV{TMP_DIR}/tos" or fatal_error "Unable to open stripped tos file: $!";
while ( $line = <TOS> ) {
chomp $line;
$line =~ s/\s+/ /g;
my ($source, $dest, $proto, $sports, $ports, $extra) = split /\s+/, $line;
fatal_error "Invalid tos file entry: \"$line\"" if $extra;
}
close TOS;
$comment = '';
}
}
sub add_rule_pair( $$$$ ) {
my ($chainref , $predicate , $target , $level ) = @_;
log_rule $level, $chainref, $target, , $predicate, if $level;
add_rule $chainref , "${predicate}-j $target";
}
sub setup_rfc1918_filteration( $ ) {
my $listref = $_[0];
my $norfc1918ref = new_standard_chain 'norfc1918';
my $rfc1918ref = new_standard_chain 'rfc1918';
my $chainref = $norfc1918ref;
log_rule $config{RFC1918_LOG_LEVEL} , $rfc1918ref , 'DROP' , '';
add_rule $rfc1918ref , '-j DROP';
if ( $config{RFC1918_STRICT} ) {
$chainref = new_standard_chain 'rfc1918d';
}
open RFC, "$ENV{TMP_DIR}/rfc1918" or fatal_error "Unable to open stripped rfc1918 file: $!";
while ( $line = <RFC> ) {
chomp $line;
$line =~ s/\s+/ /g;
my ( $networks, $target, $extra ) = split /\s+/, $line;
my $s_target;
if ( $target eq 'logdrop' ) {
$target = 'rfc1918';
$s_target = 'rfc1918';
} elsif ( $target eq 'DROP' ) {
$s_target = 'DROP';
} elsif ( $target eq 'RETURN' ) {
$s_target = $config{RFC1918_LOG_LEVEL} ? 'rfc1918d' : 'RETURN';
} else {
fatal_error "Invalid target ($target) for $networks";
}
for my $network ( split /,/, $networks ) {
add_rule $norfc1918ref , match_source_net( $network ) . "-j $s_target";
add_rule $chainref , match_orig_dest( $network ) . "-j $target" ;
}
}
close RFC;
add_rule $norfc1918ref , '-j rfc1918d' if $config{RFC1918_STRICT};
for my $hostref ( @$listref ) {
my $interface = $hostref->[0];
my $ipsec = $hostref->[1];
my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : '';
for my $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , '-m state --state NEW ' . match_source_net( $hostref->[2]) . "${policy}-j norfc1918";
}
}
}
sub setup_syn_flood_chains() {
for my $chainref ( @policy_chains ) {
my $limit = $chainref->{synparams};
if ( $limit ) {
my $level = $chainref->{loglevel};
( $limit, my $burst ) = split ':', $limit;
$burst = $burst ? "--limit-burst $burst " : '';
my $synchainref = new_chain 'filter' , syn_chain $chainref->{name};
add_rule $synchainref , "-m limit --limit $limit ${burst}-j RETURN";
log_rule_limit $level , $synchainref , $chainref->{name} , 'DROP', '-m limit --limit 5/min --limit-burst 5' , '' , 'add' , '' if $level;
add_rule $synchainref, '-j DROP';
}
}
}
sub setup_blacklist() {
my ( $level, $disposition ) = @config{'BLACKLIST_LOGLEVEL', 'BLACKLIST_DISPOSITION' };
progress_message2 " Setting up Blacklist...";
open BL, "$ENV{TMP_DIR}/blacklist" or fatal_error "Unable to open stripped blacklist file: $!";
progress_message( " Processing " . find_file 'blacklist' . '...' );
while ( $line = <BL> ) {
chomp $line;
$line =~ s/\s+/ /g;
my ( $networks, $protocol, $ports , $extra ) = split /\s+/, $line;
fatal_error "Invalid blacklist entry: \"$line\"" if $extra;
expand_rule
ensure_filter_chain( 'blacklst' , 0 ) ,
do_proto( $protocol , $ports, '' ) ,
$networks ,
'' ,
'' ,
'-j ' . ($disposition eq 'REJECT' ? 'reject' : $disposition),
$level ,
$disposition ,
'';
progress_message " \"$line\" added to blacklist";
}
close BL;
my $hosts = find_hosts_by_option 'blacklist';
my $state = $config{BLACKLISTNEWONLY} ? '-m state --state NEW,INVALID ' : '';
for my $hostref ( @$hosts ) {
my $interface = $hostref->[0];
my $ipsec = $hostref->[1];
my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : '';
my $network = $hostref->[2];
my $source = match_source_net $network;
for my $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , "${source}${state}${policy}-j blacklst";
}
progress_message " Blacklisting enabled on ${interface}:${network}";
}
}
sub process_criticalhosts() {
my $fn = find_file 'routestopped';
my @critical;
@critical = ();
open RS, "$ENV{TMP_DIR}/routestopped" or fatal_error "Unable to open stripped routestopped file: $!";
while ( $line = <RS> ) {
my $routeback = 0;
chomp $line;
$line =~ s/\s+/ /g;
my ($interface, $hosts, $options, $extra) = split /\s+/, $line;
fatal_error "Invalid routestopped file entry: \"$line\"" if $extra;
$hosts = ALLIPv4 unless $hosts && $hosts ne '-';
my @hosts;
for my $host ( split /,/, $hosts ) {
push @hosts, "$interface:$hosts";
}
$options = '-' unless $options;
unless ( $options eq '-' ) {
for my $option (split /,/, $options ) {
unless ( $option eq 'routeback' || $option eq 'source' || $option eq 'dest' ) {
if ( $option eq 'critical' ) {
push @critical, @hosts;
} else {
warning_message "Unknown routestopped option ( $option ) ignored in routestopped entry \"$line\"";
}
}
}
}
}
close RS;
\@critical;
}
sub process_routestopped() {
my $fn = find_file 'routestopped';
my ( @allhosts, %source, %dest );
@critical = ();
progress_message2 "$doing $fn...";
open RS, "$ENV{TMP_DIR}/routestopped" or fatal_error "Unable to open stripped routestopped file: $!";
while ( $line = <RS> ) {
my $routeback = 0;
chomp $line;
$line =~ s/\s+/ /g;
my ($interface, $hosts, $options, $extra) = split /\s+/, $line;
fatal_error "Invalid routestopped file entry: \"$line\"" if $extra;
$hosts = ALLIPv4 unless $hosts && $hosts ne '-';
my @hosts;
for my $host ( split /,/, $hosts ) {
push @hosts, "$interface:$hosts";
}
$options = '-' unless $options;
unless ( $options eq '-' ) {
for my $option (split /,/, $options ) {
if ( $option eq 'routeback' ) {
if ( $routeback ) {
warning_message "Duplicate 'routeback' option ignored in routestopped entry \"$line\"";
} else {
$routeback = 1;
for my $host ( split /,/, $hosts ) {
my $source = match_source_net $host;
my $dest = match_dest_net $host;
emit "run_iptables -A FORWARD -i $interface -o $interface $source $dest -j ACCEPT";
}
}
} elsif ( $option eq 'source' ) {
for my $host ( split /,/, $hosts ) {
$source{"$interface:$host"} = 1;
}
} elsif ( $option eq 'dest' ) {
for my $host ( split /,/, $hosts ) {
$dest{"$interface:$host"} = 1;
}
} else {
warning_message "Unknown routestopped option ( $option ) ignored in routestopped entry \"$line\"" $option eq 'critical';
}
}
}
push @allhosts, @hosts;
}
close RS;
for my $host ( @allhosts ) {
my ( $interface, $h ) = split /,/, $host;
my $source = match_source_net $h;
my $dest = match_dest_net $h;
emit "\$IPTABLES INPUT -i $interface $source ACCEPT";
emit "\$IPTABLES OUTPUT -o $interface $dest ACCEPT" if $config{ADMINISABSENTMINDED};
my $matched = 0;
if ( $source{$host} ) {
emit "\$IPTABLES FORWARD -i $interface $source ACCEPT";
$matched = 1;
}
if ( $dest{$host} ) {
emit "\$IPTABLES FORWARD -o $interface $dest ACCEPT";
$matched = 1;
}
unless ( $matched ) {
for my $host1 ( @allhosts ) {
unless ( $host eq $host1 ) {
my ( $interface1, $h1 ) = split /,/, $host1;
my $dest1 = match_dest_net $h1;
emit "\$IPTABLES -A FORWARD -i $interface -o $interface1 $source $dest1 -j ACCEPT";
}
}
}
}
}
sub setup_forwarding() {
if ( "\L$config{IP_FORWARDING}" eq 'on' ) {
emit 'echo 1 > /proc/sys/net/ipv4/ip_forward';
emit 'progress_message2 IP Forwarding Enabled';
} elsif ( "\L$config{IP_FORWARDING}" eq 'off' ) {
emit 'echo 0 > /proc/sys/net/ipv4/ip_forward';
emit 'progress_message2 IP Forwarding Disabled!';
}
emit '';
}
sub add_common_rules() {
my $interface;
my $chainref;
my $level;
my $target;
my $rule;
my $list;
my $chain;
my $rejectref = new_standard_chain 'reject';
new_standard_chain 'dynamic';
my $state = $config{BLACKLISTNEWONLY} ? '-m state --state NEW,INVALID' : '';
for $interface ( @interfaces ) {
for $chain ( input_chain $interface , forward_chain $interface ) {
add_rule new_standard_chain( $chain ) , "$state -j dynamic";
}
new_standard_chain output_chain( $interface );
}
$level = $env{BLACKLIST_LOG_LEVEL} || 'info';
add_rule_pair new_standard_chain( 'logdrop' ), ' ' , 'DROP' , $level ;
add_rule_pair new_standard_chain( 'logreject' ), ' ' , 'REJECT' , $level ;
setup_blacklist;
$list = find_hosts_by_option 'nosmurfs';
if ( $capabilities{ADDRTYPE} ) {
$chainref = new_standard_chain 'smurfs';
add_rule_pair $chainref, '-m addrtype --src-type BROADCAST ', 'DROP', $config{SMURF_LOG_LEVEL} ;
add_rule_pair $chainref, '-m addrtype --src-type MULTICAST ', 'DROP', $config{SMURF_LOG_LEVEL} ;
add_rule $rejectref , '-m addrtype --src-type BROADCAST -j DROP';
add_rule $rejectref , '-m addrtype --src-type MULTICAST -j DROP';
} elsif ( @$list ) {
fatal_error "The nosmurfs option requires Address Type Match in your kernel and iptables";
}
if ( @$list ) {
progress_message2 ' Adding Anti-smurf Rules';
for my $hostref ( @$list ) {
$interface = $hostref->[0];
my $ipsec = $hostref->[1];
my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : '';
for $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , '-m state --state NEW,INVALID ' . match_source_net( $hostref->[2]) . "${policy}-j smurfs";
}
}
}
add_rule $rejectref , '-p tcp -j REJECT --reject-with tcp-reset';
if ( $capabilities{ENHANCED_REJECT} ) {
add_rule $rejectref , '-p udp -j REJECT';
add_rule $rejectref, '-p icmp -j REJECT --reject-with icmp-host-unreachable';
add_rule $rejectref, '-j REJECT --reject-with icmp-host-prohibited';
} else {
add_rule $rejectref , '-j REJECT';
}
$list = find_interfaces_by_option 'dhcp';
if ( @$list ) {
progress_message2 ' Adding rules for DHCP';
for $interface ( @$list ) {
for $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , '-p udp --dport 67:68 -j ACCEPT';
}
add_rule $filter_table->{forward_chain $interface} , "-p udp -o $interface --dport 67:68 -j ACCEPT" if $interfaces{$interface}{options}{routeback};
}
}
$list = find_hosts_by_option 'norfc1918';
if ( @$list ) {
progress_message2 ' Enabling RFC1918 Filtering';
setup_rfc1918_filteration $list;
}
$list = find_hosts_by_option 'tcpflags';
if ( @$list ) {
my $disposition;
progress_message2 " $doing TCP Flags checking...";
$chainref = new_standard_chain 'tcpflags';
if ( $config{TCP_FLAGS_LOG_LEVEL} ) {
my $logflagsref = new_standard_chain 'logflags';
my $savelogparms = $env{LOGPARMS};
$env{LOGPARMS} = "$env{LOGPARMS} --log-ip-options" unless $config{TCP_FLAGS_LOG_LEVEL} eq 'ULOG';
log_rule $config{TCP_FLAGS_LOG_LEVEL} , $logflagsref , $config{TCP_FLAGS_DISPOSITION}, '';
$env{LOGPARMS} = $savelogparms;
if ( $config{TCP_FLAGS_DISPOSITION} eq 'REJECT' ) {
add_rule $logflagsref , '-j REJECT --reject-with tcp-reset';
} else {
add_rule $logflagsref , "-j $config{TCP_FLAGS_DISPOSITION}";
}
$disposition = 'logflags';
} else {
$disposition = $config{TCP_FLAGS_DISPOSITION};
}
add_rule $chainref , "-p tcp --tcp-flags ALL FIN,URG,PSH -j $disposition";
add_rule $chainref , "-p tcp --tcp-flags ALL NONE -j $disposition";
add_rule $chainref , "-p tcp --tcp-flags SYN,RST SYN,RST -j $disposition";
add_rule $chainref , "-p tcp --tcp-flags SYN,FIN SYN,FIN -j $disposition";
add_rule $chainref , "-p tcp --syn --sport 0 -j $disposition";
for my $hostref ( @$list ) {
$interface = $hostref->[0];
my $ipsec = $hostref->[1];
my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : '';
for $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , '-p tcp ' . match_source_net( $hostref->[2]) . "${policy}-j tcpflags";
}
}
}
if ( $config{DYNAMIC_ZONES} ) {
for $interface ( @interfaces) {
for $chain ( @{dynamic_chains $interface} ) {
new_standard_chain $chain;
}
}
(new_chain 'nat' , $chain = dynamic_in($interface) )->{referenced} = 1;
add_rule $filter_table->{input_chain $interface}, "-j $chain";
add_rule $filter_table->{forward_chain $interface}, '-j ' . dynamic_fwd $interface;
add_rule $filter_table->{output_chain $interface}, '-j ' . dynamic_out $interface;
}
$list = find_interfaces_by_option 'upnp';
if ( @$list ) {
progress_message2 ' $doing UPnP';
(new_chain 'nat', 'UPnP')->{referenced} = 1;
for $interface ( @$list ) {
add_rule $nat_table->{PREROUTING} , "-i $interface -j UPnP";
}
}
setup_syn_flood_chains;
setup_forwarding;
}
my %maclist_targets = ( ACCEPT => { target => 'RETURN' , mangle => 1 } ,
REJECT => { target => 'reject' , mangle => 0 } ,
DROP => { target => 'DROP' , mangle => 1 } );
sub setup_mac_lists( $ ) {
my $phase = $_[0];
my %maclist_interfaces;
my $table = $config{MACLIST_TABLE};
my $maclist_hosts = find_hosts_by_option 'maclist';
for my $hostref ( $maclist_hosts ) {
$maclist_interfaces{ $hostref->[0][0] } = 1;
}
my @maclist_interfaces = ( sort keys %maclist_interfaces );
progress_message " $doing MAC Verification for @maclist_interfaces -- Phase $phase...";
if ( $phase == 1 ) {
for my $interface ( @maclist_interfaces ) {
my $chainref = new_chain $table , mac_chain $interface;
add_rule $chainref , '-s 0.0.0.0 -d 255.255.255.255 -p udp --dport 67:68 -j RETURN'
if ( $table eq 'mangle' ) && $interfaces{$interface}{options}{dhcp};
if ( $config{MACLIST_TTL} ) {
my $chain1ref = new_chain $table, macrecent_target $interface;
my $chain = $chainref->{name};
add_rule $chainref, "-m recent --rcheck --seconds $config{MACLIST_TTL} --name $chain -j RETURN";
add_rule $chainref, "-j $chain1ref->{name}";
add_rule $chainref, "-m recent --update --name $chain -j RETURN";
add_rule $chainref, "-m recent --set --name $chain";
}
}
open MAC, "$ENV{TMP_DIR}/maclist" or fatal_error "Unable to open stripped maclist file: $!";
while ( $line = <MAC> ) {
chomp $line;
$line =~ s/\s+/ /g;
my ( $disposition, $interface, $mac, $addresses , $extra ) = split /\s+/, $line;
if ( $disposition eq 'COMMENT' ) {
if ( $capabilities{COMMENTS} ) {
( $comment = $line ) =~ s/^\s*COMMENT\s*//;
$comment =~ s/\s*$//;
} else {
warning_message "COMMENT ignored -- requires comment support in iptables/Netfilter";
}
} else {
fatal_error "Invalid maclist entry: \"$line\"" if $extra;
( $disposition, my $level ) = split /:/, $disposition;
my $targetref = $maclist_targets{$disposition};
fatal_error "Invalid DISPOSITION ( $disposition) in rule \"$line\"" if ( $table eq 'mangle' ) && ! $targetref->{mangle};
fatal_error "No hosts on $interface have the maclist option specified: \"$line\"" unless $maclist_interfaces{$interface};
my $chainref = $chain_table{$table}{( $config{MACLIST_TTL} ? macrecent_target $interface : mac_chain $interface )};
$mac = '' unless $mac && ( $mac ne '-' );
$addresses = '' unless $addresses && ( $addresses ne '-' );
fatal_error "You must specify a MAC address or an IP address" unless $mac || $addresses;
$mac = mac_match $mac if $mac;
if ( $addresses ) {
for my $address ( split ',', $addresses ) {
my $source = match_source_net $address;
log_rule_limit $level, $chainref , mac_chain( $interface) , $disposition, '', '', 'add' , "${mac}${source}" if $level;
add_rule $chainref , "${mac}${source}-j $targetref->{target}";
}
} else {
log_rule_limit $level, $chainref , mac_chain( $interface) , $disposition, '', '', 'add' , $mac if $level;
add_rule $chainref , "$mac-j $targetref->{target}";
}
progress_message " Maclist entry \"$line\" $done";
}
}
close MAC;
$comment = '';
#
# Generate jumps from the input and forward chains
#
for my $hostref ( @$maclist_hosts ) {
my $interface = $hostref->[0];
my $ipsec = $hostref->[1];
my $policy = $capabilities{POLICY_MATCH} ? "-m policy --pol $ipsec --dir in " : '';
my $source = match_source_net $hostref->[2];
my $target = mac_chain $interface;
if ( $table eq 'filter' ) {
for my $chain ( @{first_chains $interface}) {
add_rule $filter_table->{$chain} , "${source}-m state --statue NEW ${policy}-j $target";
}
} else {
add_rule $mangle_table->{PREROUTING}, "-i $interface ${source}-m state --state NEW ${policy}-j $target";
}
}
} else {
my $target = $env{MACLIST_TARGET};
my $level = $config{MACLIST_LOG_LEVEL};
my $disposition = $config{MACLIST_DISPOSITION};
for my $interface ( @maclist_interfaces ) {
my $chainref = $chain_table{$table}{( $config{MACLIST_TTL} ? macrecent_target $interface : mac_chain $interface )};
my $chain = mac_chain $interface;
log_rule_limit $level, $chainref , $chain , $disposition, '', '', 'add', '';
add_rule $chainref, "-j $target";
}
}
}
sub process_rule1 ( $$$$$$$$$ );
#
# Expand a macro rule from the rules file
#
sub process_macro ( $$$$$$$$$$$ ) {
my ($macrofile, $target, $param, $source, $dest, $proto, $ports, $sports, $origdest, $rate, $user) = @_;
my $standard = ( $macrofile =~ /^($env{SHAREDIR})/ );
progress_message "..Expanding Macro $macrofile...";
open M, $macrofile or fatal_error "Unable to open $macrofile: $!";
while ( $line = <M> ) {
chomp $line;
next if $line =~ /^\s*#/;
next if $line =~ /^\s*$/;
$line =~ s/\s+/ /g;
$line =~ s/#.*$//;
$line = expand_shell_variables $line unless $standard;
my ( $mtarget, $msource, $mdest, $mproto, $mports, $msports, $mrate, $muser ) = split /\s+/, $line;
$mtarget = merge_levels $target, $mtarget;
if ( $mtarget =~ /^PARAM:?/ ) {
fatal_error 'PARAM requires that a parameter be supplied in macro invocation' unless $param;
$mtarget = substitute_action $param, $mtarget;
}
my $action = isolate_basic_target $mtarget;
my $actiontype = $targets{$action};
if ( $actiontype & ACTION ) {
unless ( $usedactions{$action} ) {
createactionchain $mtarget;
$usedactions{$mtarget} = 1;
}
$mtarget = find_logactionchain $mtarget;
} else {
fatal_error "Invalid Action ($mtarget) in rule \"$line\"" unless $actiontype & STANDARD;
}
if ( $msource ) {
if ( ( $msource eq '-' ) || ( $msource eq 'SOURCE' ) ) {
$msource = $source || '';
} elsif ( $msource eq 'DEST' ) {
$msource = $dest || '';
} else {
$msource = merge_macro_source_dest $msource, $source;
}
} else {
$msource = '';
}
$msource = '' if $msource eq '-';
if ( $mdest ) {
if ( ( $mdest eq '-' ) || ( $mdest eq 'DEST' ) ) {
$mdest = $dest || '';
} elsif ( $mdest eq 'SOURCE' ) {
$mdest = $source || '';
} else {
$mdest = merge_macro_source_dest $mdest, $dest;
}
} else {
$mdest = '';
}
$mdest = '' if $mdest eq '-';
$mproto = merge_macro_column $mproto, $proto;
$mports = merge_macro_column $mports, $ports;
$msports = merge_macro_column $msports, $sports;
$mrate = merge_macro_column $mrate, $rate;
$muser = merge_macro_column $muser, $user;
process_rule1 $mtarget, $msource, $mdest, $mproto, $mports, $msports, $origdest, $rate, $user;
progress_message " Rule \"$line\" $done"; }
close M;
progress_message '..End Macro'
}
#
# Once a rule has been completely resolved by macro expansion, it is processed by this function.
#
sub process_rule1 ( $$$$$$$$$ ) {
my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user ) = @_;
my ( $action, $loglevel) = split_action $target;
my $rule = '';
my $actionchainref;
$ports = '' unless defined $ports;
$sports = '' unless defined $sports;
$origdest = '' unless defined $origdest;
$ratelimit = '' unless defined $ratelimit;
$user = '' unless defined $user;
#
# Determine the validity of the action
#
my $actiontype = $targets{$action} || find_macro( isolate_basic_target $action );
fatal_error "Unknown action ($action) in rule \"$line\"" unless $actiontype;
if ( $actiontype == MACRO ) {
process_macro
$macros{isolate_basic_target $action}, $
target ,
(split '/', $action)[1] ,
$source,
$dest,
$proto,
$ports,
$sports,
$origdest,
$ratelimit,
$user;
return;
}
#
# We can now dispense with the postfix characters
#
$action =~ s/[\+\-!]$//;
#
# Mark target as used
#
if ( $actiontype & ACTION ) {
unless ( $usedactions{$target} ) {
$usedactions{$target} = 1;
createactionchain $target;
}
}
#
# Take care of irregular syntax and targets
#
if ( $actiontype & REDIRECT ) {
if ( $dest eq '-' ) {
$dest = "$firewall_zone";
} else {
$dest = "$firewall_zone" . '::' . "$dest";
}
} elsif ( $action eq 'REJECT' ) {
$action = 'reject';
} elsif ( $action eq 'CONTINUE' ) {
$action = 'RETURN';
}
#
# Isolate and validate source and destination zones
#
my $sourcezone;
my $destzone;
if ( $source =~ /^(.+?):(.*)/ ) {
$sourcezone = $1;
$source = $2;
} else {
$sourcezone = $source;
$source = ALLIPv4;
}
if ( $dest =~ /^(.+?):(.*)/ ) {
$destzone = $1;
$dest = $2;
} else {
$destzone = $dest;
$dest = ALLIPv4;
}
fatal_error "Unknown source zone ($sourcezone) in rule \"$line\"" unless $zones{$sourcezone};
fatal_error "Unknown destination zone ($destzone) in rule \"$line\"" unless $zones{$destzone};
#
# Take care of chain
#
my $chain = "${sourcezone}2${destzone}";
my $chainref = ensure_filter_chain $chain, 1;
#
# Validate Policy
#
my $policy = $chainref->{policy};
fatal_error "No policy defined from $sourcezone to zone $destzone" unless $policy;
fatal_error "Rules may not override a NONE policy: rule \"$line\"" if $policy eq 'NONE';
#
# Generate Fixed part of the rule
#
$rule = do_proto $proto, $ports, $sports . do_ratelimit( $ratelimit ) . ( do_user $user );
$origdest = ALLIPv4 unless $origdest and $origdest ne '-';
#
# Generate NAT rule(s), if any
#
if ( $actiontype & NATRULE ) {
my ( $server, $serverport , $natchain );
fatal_error "$target rules not allowed in the $section SECTION" if $section ne 'NEW';
#
# Isolate server port
#
if ( $dest =~ /^(.*)(:(\d+))$/ ) {
$server = $1;
$serverport = $3;
} else {
$server = $dest;
$serverport = '';
}
#
# After DNAT, dest port will be the server port
#
$ports = $serverport if $serverport;
fatal_error "A server must be specified in the DEST column in $action rules: \"$line\"" unless ( $actiontype & REDIRECT ) || $server;
fatal_error "Invalid server ($server), rule: \"$line\"" if $server =~ /:/;
#
# Generate the target
#
my $target = '';
if ( $action eq 'SAME' ) {
fatal_error 'Port mapping not allowed in SAME rules' if $serverport;
$target = '-j SAME ';
for my $serv ( split /,/, $server ) {
$target .= "--to $serv ";
}
$serverport = $ports;
} elsif ( $action eq ' -j DNAT' ) {
$serverport = ":$serverport" if $serverport;
for my $serv ( split /,/, $server ) {
$target .= "--to ${serv}${serverport} ";
}
} else {
$target = '-j REDIRECT --to-port ' . ( $serverport ? $serverport : $ports );
}
#
# And generate the nat table rule(s)
#
expand_rule
ensure_chain ('nat' , $zones{$sourcezone}{type} eq 'firewall' ? 'OUTPUT' : dnat_chain $sourcezone ) ,
$rule ,
$source ,
$origdest ,
'' ,
$target ,
$loglevel ,
$action ,
$serverport ? do_proto( $proto, '', '' ) : '';
#
# After NAT, the destination port will be the server port; Also, we log NAT rules in the nat table rather than in the filter table.
#
unless ( $actiontype & NATONLY ) {
$rule = do_proto $proto, $ports, $sports . do_ratelimit( $ratelimit ) . do_user $user;
$loglevel = '';
}
} elsif ( $actiontype & NONAT ) {
#
# NONAT or ACCEPT+ -- May not specify a destination interface
#
fatal_error "Invalid DEST ($dest) in $action rule \"$line\"" if $dest =~ /:/;
expand_rule
ensure_chain ('nat' , $zones{$sourcezone}{type} eq 'firewall' ? 'OUTPUT' : dnat_chain $sourcezone) ,
$rule ,
$source ,
$dest ,
'' ,
'-j RETURN ' ,
$loglevel ,
$action ,
'';
}
#
# Add filter table rule, unless this is a NATONLY rule type
#
unless ( $actiontype & NATONLY ) {
if ( $actiontype & ACTION ) {
$action = (find_logactionchain $target)->{name};
$loglevel = '';
}
expand_rule
ensure_chain ('filter', $chain ) ,
$rule ,
$source ,
$dest ,
$origdest ,
"-j $action " ,
$loglevel ,
$action ,
'';
}
}
#
# Process a Record in the rules file
#
sub process_rule ( $$$$$$$$$ ) {
my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user ) = @_;
my $intrazone = 0;
my $includesrcfw = 1;
my $includedstfw = 1;
my $optimize = $config{OPTIMIZE};
my $thisline = $line;
#
# Section Names are optional so once we get to an actual rule, we need to be sure that
# we close off any missing sections.
#
unless ( $sectioned ) {
finish_section 'ESTABLISHED,RELATED';
$section = 'NEW';
$sectioned = 1;
}
#
# Handle Wildcards
#
if ( $source =~ /^all[-+]/ ) {
if ( $source eq 'all+' ) {
$source = 'all';
$intrazone = 1;
} elsif ( ( $source eq 'all+-' ) || ( $source eq 'all-+' ) ) {
$source = 'all';
$intrazone = 1;
$includesrcfw = 0;
} elsif ( $source eq 'all-' ) {
$source = 'all';
$includesrcfw = 0;
}
}
if ( $dest =~ /^all[-+]/ ) {
if ( $dest eq 'all+' ) {
$dest = 'all';
$intrazone = 1;
} elsif ( ( $dest eq 'all+-' ) || ( $dest eq 'all-+' ) ) {
$dest = 'all';
$intrazone = 1;
$includedstfw = 0;
} elsif ( $source eq 'all-' ) {
$dest = 'all';
$includedstfw = 0;
}
}
my $action = isolate_basic_target $target;
$optimize = 0 if $action =~ /!^/;
if ( $source eq 'all' ) {
for my $zone ( @zones ) {
if ( $includesrcfw || ( $zones{$zone}{type} ne 'firewall' ) ) {
if ( $dest eq 'all' ) {
for my $zone1 ( @zones ) {
if ( $includedstfw || ( $zones{$zone1}{type} ne 'firewall' ) ) {
if ( $intrazone || ( $zone ne $zone1 ) ) {
my $policychainref = $filter_table->{"${zone}2${zone1}"}{policychain};
fatal_error "No policy from zone $zone to zone $zone1" unless $policychainref;
if ( ( ( my $policy ) = $policychainref->{policy} ) ne 'NONE' ) {
if ( $optimize > 0 ) {
my $loglevel = $policychainref->{loglevel};
if ( $loglevel ) {
next if $target eq "${policy}:$loglevel}";
} else {
next if $action eq $policy;
}
}
process_rule1 $target, $zone, $zone1 , $proto, $ports, $sports, $origdest, $ratelimit, $user;
}
}
}
}
} else {
process_rule1 $target, $zone, $dest , $proto, $ports, $sports, $origdest, $ratelimit, $user;
}
}
}
} elsif ( $dest eq 'all' ) {
for my $zone1 ( @zones ) {
my $zone = ( split /:/, $source )[0];
if ( ( $includedstfw || ( $zones{$zone1}{type} ne 'firewall') ) &&( ( $zone ne $zone1 ) || $intrazone) ) {
process_rule1 $target, $source, $zone1 , $proto, $ports, $sports, $origdest, $ratelimit, $user;
}
}
} else {
process_rule1 $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user;
}
progress_message " Rule \"$thisline\" $done";
}
#
# Process the Rules File
#
sub process_rules() {
open RULES, "$ENV{TMP_DIR}/rules" or fatal_error "Unable to open stripped rules file: $!";
while ( $line = <RULES> ) {
chomp $line;
$line =~ s/\s+/ /g;
my ( $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user, $extra ) = split /\s+/, $line;
if ( $target eq 'COMMENT' ) {
if ( $capabilities{COMMENTS} ) {
( $comment = $line ) =~ s/^\s*COMMENT\s*//;
$comment =~ s/\s*$//;
} else {
warning_message "COMMENT ignored -- requires comment support in iptables/Netfilter";
}
} elsif ( $target eq 'SECTION' ) {
fatal_error "Invalid SECTION $source" unless defined $sections{$source};
fatal_error "Duplicate or out of order SECTION $source" if $sections{$source};
fatal_error "Invalid Section $source $dest" if $dest;
$sectioned = 1;
$sections{$source} = 1;
if ( $section eq 'RELATED' ) {
$sections{ESTABLISHED} = 1;
finish_section 'ESTABLISHED';
} elsif ( $section eq 'NEW' ) {
@sections{'ESTABLISHED','RELATED'} = ( 1, 1 );
finish_section ( ( $section eq 'RELATED' ) ? 'RELATED' : 'ESTABLISHED,RELATED' );
}
$section = $source;
} else {
fatal_error "Invalid rules file entry: \"$line\"" if $extra;
process_rule $target, $source, $dest, $proto, $ports, $sports, $origdest, $ratelimit, $user;
}
}
close RULES;
$comment = '';
$section = 'DONE';
}
#
# To quote an old comment, generate_matrix makes a sows ear out of a silk purse.
#
# The biggest disadvantage of the zone-policy-rule model used by Shorewall is that it doesn't scale well as the number of zones increases (Order N**2 where N = number of zones).
# A major goal of the rewrite of the compiler in Perl was to restrict those scaling effects to this functions and the rules that it generates.
#
# The function traverses the full "source-zone X destination-zone" matrix and generates the rules necessary to direct traffic through the right set of rules.
#
sub generate_matrix() {
#
# Helper functions for generate_matrix()
#-----------------------------------------
#
# Return the target for rules from the $zone to $zone1.
#
sub rules_target( $$ ) {
my ( $zone, $zone1 ) = @_;
my $chain = "${zone}2${zone1}";
my $chainref = $filter_table->{$chain};
return $chain if $chainref && $chainref->{referenced};
return 'ACCEPT' if $zone eq $zone1;
if ( $chainref->{policy} ne 'CONTINUE' ) {
my $policyref = $chainref->{policychain};
return $policyref->{name} if $policyref;
fatal_error "No policy defined for zone $zone to zone $zone1";
}
'';
}
#
# Add a jump to the passed chain ($chainref) to the dynamic zone chain for the passed zone.
#
sub create_zone_dyn_chain( $$ ) {
my ( $zone , $chainref ) = @_;
my $name = "${zone}_dyn";
new_standard_chain $name;
add_rule $chainref, "-j $name";
}
#
# Insert the passed exclusions at the front of the passed chain.
#
sub insert_exclusions( $$ ) {
my ( $chainref, $exclusionsref ) = @_;
my $num = 1;
for my $host ( @{$exclusionsref} ) {
my ( $interface, $net ) = split /:/, $host;
insert_rule $chainref , $num++, "-i $interface " . match_source_net( $host ) . '-j RETURN';
}
}
#
# Add the passed exclusions at the end of the passed chain.
#
sub add_exclusions ( $$ ) {
my ( $chainref, $exclusionsref ) = @_;
for my $host ( @{$exclusionsref} ) {
my ( $interface, $net ) = split /:/, $host;
add_rule $chainref , "-i $interface " . match_source_net( $host ) . '-j RETURN';
}
}
#
# Generate_Matrix() Starts Here
#
my $prerouting_rule = 1;
my $postrouting_rule = 1;
my $exclusion_seq = 1;
my %chain_exclusions;
my %policy_exclusions;
for my $interface ( @interfaces ) {
addnatjump 'POSTROUTING' , snat_chain( $interface ), "-o $interface ";
}
if ( $config{DYNAMIC_ZONES} ) {
for my $interface ( @interfaces ) {
addnatjump 'PREROUTING' , dynamic_in( $interface ), "-i $interface ";
}
}
addnatjump 'PREROUTING' , 'nat_in' , '';
addnatjump 'POSTROUTING' , 'nat_out' , '';
for my $interface ( @interfaces ) {
addnatjump 'PREROUTING' , input_chain( $interface ) , "-i $interface ";
addnatjump 'POSTROUTING' , output_chain( $interface ) , "-o $interface ";
}
for my $zone ( grep $zones{$_}{options}{complex} , @zones ) {
my $frwd_ref = new_standard_chain "${zone}_frwd";
my $zoneref = $zones{$zone};
my $exclusions = $zoneref->{exclusions};
if ( @$exclusions ) {
my $num = 1;
my $in_ref = new_standard_chain "${zone}_input";
my $out_ref = new_standard_chain "${zone}_output";
add_rule ensure_filter_chain( "${zone}2${zone}", 1 ) , '-j ACCEPT' if rules_target $zone, $zone eq 'ACCEPT';
for my $host ( @$exclusions ) {
my ( $interface, $net ) = split /:/, $host;
add_rule $frwd_ref , "-i $interface -s $net -j RETURN";
add_rule $in_ref , "-i $interface -s $net -j RETURN";
add_rule $out_ref , "-i $interface -s $net -j RETURN";
}
if ( $capabilities{POLICY_MATCH} ) {
my $type = $zoneref->{type};
my $source_ref = $zoneref->{hosts}{ipsec} || [];
create_zone_dyn_chain $zone, $frwd_ref && $config{DYNAMIC_ZONES} && (@$source_ref || $type ne 'ipsec4' );
while ( my ( $interface, $arrayref ) = each %$source_ref ) {
for my $hostref ( @{$arrayref} ) {
my $ipsec_match = match_ipsec_in $zone , $hostref;
for my $net ( @{$hostref->{hosts}} ) {
add_rule
find_chainref( 'filter' , forward_chain $interface ) ,
match_source_net $net . $ipsec_match . "-j $frwd_ref->n{name}";
}
}
}
}
}
}
#
# Main source-zone matrix-generation loop
#
for my $zone ( grep ( $zones{$_}{type} ne 'firewall' , @zones ) ) {
my $zoneref = $zones{$zone};
my $source_hosts_ref = $zoneref->{hosts};
my $chain1 = rules_target $firewall_zone , $zone;
my $chain2 = rules_target $zone, $firewall_zone;
my $complex = $zoneref->{options}{complex} || 0;
my $type = $zoneref->{type};
my $exclusions = $zoneref->{exclusions};
my $need_broadcast = {}; ### Fixme ###
my $frwd_ref = 0;
my $chain = 0;
if ( $complex ) {
$frwd_ref = $filter_table->{"${zone}_frwd"};
my $dnat_ref = ensure_chain 'nat' , dnat_chain( $zone );
if ( @$exclusions ) {
insert_exclusions $dnat_ref, $exclusions if $dnat_ref->{referenced};
}
}
#
# Take care of PREROUTING, INPUT and OUTPUT jumps
#
for my $typeref ( values %$source_hosts_ref ) {
while ( my ( $interface, $arrayref ) = each %$typeref ) {
for my $hostref ( @$arrayref ) {
my $ipsec_in_match = match_ipsec_in $zone , $hostref;
my $ipsec_out_match = match_ipsec_out $zone , $hostref;
for my $net ( @{$hostref->{hosts}} ) {
my $source = match_source_net $net;
my $dest = match_dest_net $net;
if ( $chain1 ) {
if ( @$exclusions ) {
add_rule $filter_table->{output_chain $interface} , $dest . $ipsec_out_match . "-j ${zone}_output";
add_rule $filter_table->{"${zone}_output"} , "-j $chain1";
} else {
add_rule $filter_table->{output_chain $interface} , $dest . $ipsec_out_match . "-j $chain1";
}
}
insertnatjump 'PREROUTING' , dnat_chain $zone, \$prerouting_rule, ( "-i $interface " . $source . $ipsec_in_match );
if ( $chain2 ) {
if ( @$exclusions ) {
add_rule $filter_table->{input_chain $interface}, $source . $ipsec_in_match . "-j ${zone}_input";
add_rule $filter_table->{"${zone}_input"} , "-j $chain2";
} else {
add_rule $filter_table->{input_chain $interface}, $source . $ipsec_in_match . "-j $chain2";
}
}
add_rule $filter_table->{forward_chain $interface} , $source . $ipsec_in_match . "-j $frwd_ref->{name}"
if $complex && $hostref->{ipsec} ne 'ipsec';
}
}
}
}
#
# F O R W A R D I N G
#
my @dest_zones;
my $last_chain = '';
if ( $config{OPTIMIZE} > 0 ) {
my @temp_zones;
ZONE1:
for my $zone1 ( grep $zones{$_}{type} ne 'firewall' , @zones ) {
my $zone1ref = $zones{$zone1};
my $policy = $filter_table->{"${zone}2${zone1}"}->{policy};
next if $policy eq 'NONE';
my $chain = rules_target $zone, $zone1;
next unless $chain;
if ( $zone eq $zone1 ) {
#
# One thing that the Llama fails to mention is that evaluating a hash in a numeric context produces a warning.
#
no warnings;
next if ( %{ $zoneref->{interfaces}} < 2 ) && ! ( $zoneref->{options}{routeback} || @$exclusions );
}
if ( $chain =~ /2all$/ ) {
if ( $chain ne $last_chain ) {
$last_chain = $chain;
push @dest_zones, @temp_zones;
@temp_zones = ( $zone1 );
} elsif ( $policy eq 'ACCEPT' ) {
push @temp_zones , $zone1;
} else {
$last_chain = $chain;
@temp_zones = ( $zone1 );
}
} else {
push @dest_zones, @temp_zones, $zone1;
@temp_zones = ();
$last_chain = '';
}
}
if ( $last_chain && @temp_zones == 1 ) {
push @dest_zones, @temp_zones;
$last_chain = '';
}
} else {
@dest_zones = grep $zones{$_}{type} ne 'firewall' , @zones ;
}
#
# Here it is -- THE BIG UGLY!!!!!!!!!!!!
#
# We now loop through the destination zones creating jumps to the rules chain for each source/dest combination.
# @dest_zones is the list of destination zones that we need to handle from this source zone
#
ZONE1:
for my $zone1 ( @dest_zones ) {
my $zone1ref = $zones{$zone1};
my $policy = $filter_table->{"${zone}2${zone1}"}->{policy};
next if $policy eq 'NONE';
my $chain = rules_target $zone, $zone1;
next unless $chain;
my $num_ifaces = 0;
if ( $zone eq $zone1 ) {
#
# One thing that the Llama fails to mention is that evaluating a hash in a numeric context produces a warning.
#
no warnings;
next ZONE1 if ( $num_ifaces = %{$zoneref->{interfaces}} ) < 2 && ! ( $zoneref->{options}{routeback} || @$exclusions );
}
my $chainref = $filter_table->{$chain};
my $exclusions1 = $zone1ref->{exclusions};
my $dest_hosts_ref = $zone1ref->{hosts};
if ( @$exclusions1 ) {
if ( $chain eq "all2$zone1" ) {
unless ( $chain_exclusions{$chain} ) {
$chain_exclusions{$chain} = 1;
insert_exclusions $chainref , $exclusions1;
}
} elsif ( $chain =~ /2all$/ ) {
my $chain1 = $policy_exclusions{"${chain}_${zone1}"};
unless ( $chain ) {
$chain1 = newexclusionchain;
$policy_exclusions{"${chain}_${zone1}"} = $chain1;
my $chain1ref = ensure_filter_chain $chain1, 0;
add_exclusions $chain1ref, $exclusions1;
add_rule $chain1ref, "-j $chain";
}
$chain = $chain1;
} else {
insert_exclusions $chainref , $exclusions1;
}
}
if ( $complex ) {
for my $typeref ( values %$dest_hosts_ref ) {
while ( my ( $interface , $arrayref ) = each %$typeref ) {
for my $hostref ( @$arrayref ) {
if ( $zone ne $zone1 || $num_ifaces > 1 || $hostref->{options}{routeback} ) {
my $ipsec_out_match = match_ipsec_out $zone1 , $hostref;
for my $net ( @{$hostref->{hosts}} ) {
add_rule $frwd_ref, "-o $interface " . match_dest_net($net) . $ipsec_out_match . "-j $chain";
}
}
}
}
}
} else {
for my $typeref ( values %$source_hosts_ref ) {
while ( my ( $interface , $arrayref ) = each %$typeref ) {
my $chain3ref = $filter_table->{forward_chain $interface};
for my $hostref ( @$arrayref ) {
for my $net ( @{$hostref->{hosts}} ) {
my $source_match = match_source_net $net;
for my $type1ref ( values %$dest_hosts_ref ) {
while ( my ( $interface1, $array1ref ) = each %$type1ref ) {
for my $host1ref ( @$array1ref ) {
my $ipsec_out_match = match_ipsec_out $zone1 , $host1ref;
for my $net1 ( @{$host1ref->{hosts}} ) {
unless ( $interface eq $interface1 && $net eq $net1 && ! $host1ref->{options}{routeback} ) {
add_rule $chain3ref, "-o $interface1 " . $source_match . match_dest_net($net1) . $ipsec_out_match . "-j $chain";
}
}
}
}
}
}
}
}
}
}
#
# E N D F O R W A R D I N G
#
# Now add (an) unconditional jump(s) to the last unique policy-only chain determined above, if any
#
if ( $last_chain ) {
if ( $complex ) {
add_rule $frwd_ref , "-j $last_chain";
} else {
for my $typeref ( values %$source_hosts_ref ) {
while ( my ( $interface , $arrayref ) = each %$typeref ) {
my $chain2ref = $filter_table->{forward_chain $interface};
for my $hostref ( @$arrayref ) {
for my $net ( @{$hostref->{hosts}} ) {
add_rule $chain2ref, match_source_net($net) . "-j $last_chain";
}
}
}
}
}
}
}
}
#
# Now add the jumps to the interface chains from FORWARD, INPUT, OUTPUT and POSTROUTING
#
for my $interface ( @interfaces ) {
add_rule $filter_table->{FORWARD} , "-i $interface -j " . forward_chain $interface;
add_rule $filter_table->{INPUT} , "-i $interface -j " . input_chain $interface;
add_rule $filter_table->{OUTPUT} , "-o $interface -j " . output_chain $interface;
addnatjump 'POSTROUTING' , masq_chain( $interface ) , "-o $interface ";
}
my $chainref = $filter_table->{"${firewall_zone}2${firewall_zone}"};
add_rule $filter_table->{OUTPUT} , "-o lo -j " . ($chainref->{referenced} ? "$chainref->{name}" : 'ACCEPT' );
add_rule $filter_table->{INPUT} , '-i lo -j ACCEPT';
complete_standard_chain $filter_table->{INPUT} , 'all' , $firewall_zone;
complete_standard_chain $filter_table->{OUTPUT} , $firewall_zone , 'all';
complete_standard_chain $filter_table->{FORWARD} , 'all' , 'all';
my %builtins = ( mangle => [ qw/PREROUTING INPUT FORWARD POSTROUTING/ ] ,
nat=> [ qw/PREROUTING OUTPUT POSTROUTING/ ] ,
filter=> [ qw/INPUT FORWARD OUTPUT/ ] );
if ( $config{LOGALLNEW} ) {
for my $table qw/mangle nat filter/ {
for my $chain ( @{$builtins{$table}} ) {
log_rule_limit
$config{LOGALLNEW} ,
$chain_table{$table}{$chain} ,
$table ,
$chain ,
'' ,
'' ,
'insert' ,
'-m state --state NEW';
}
}
}
}
1;