# # 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 Shorewall::Proc; use strict; our @ISA = qw(Exporter); our @EXPORT = qw( add_common_rules setup_mac_lists process_criticalhosts process_routestopped process_rules generate_matrix setup_mss dump_rule_chains ); our @EXPORT_OK = qw( process_rule process_rule1 ); our @VERSION = 1.00; # # Keep track of chains for the /var/lib/shorewall[-lite]/chains file # my @rule_chains; # # 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 = ) { 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 = ) { 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 $hosts = find_hosts_by_option 'blacklist'; if ( @$hosts ) { my ( $level, $disposition ) = @config{'BLACKLIST_LOGLEVEL', 'BLACKLIST_DISPOSITION' }; progress_message2 " Setting up Blacklist..."; new_standard_chain 'blacklst'; my $target = $disposition eq 'REJECT' ? 'reject' : $disposition; if ( $level ) { my $chainref = new_standard_chain 'blacklog'; log_rule_limit( $level , $chainref , 'blacklst' , $disposition , "$env{LOGLIMIT}" , '', 'add', '' ); add_rule $chainref, "-j $target" ; $target = 'blacklog'; } if ( -s "$ENV{TMP_DIR}/blacklist" ) { open BL, "$ENV{TMP_DIR}/blacklist" or fatal_error "Unable to open stripped blacklist file: $!"; progress_message( " Processing " . find_file 'blacklist' . '...' ); while ( $line = ) { 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 ) , NO_RESTRICT , do_proto( $protocol , $ports, '' ) , $networks , '' , '' , "-j $target" , '' , $disposition , ''; progress_message " \"$line\" added to blacklist"; } } close BL; 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 = ) { 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 ); progress_message2 "$doing $fn..."; open RS, "$ENV{TMP_DIR}/routestopped" or fatal_error "Unable to open stripped routestopped file: $!"; while ( $line = ) { 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\"" unless $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 add_common_rules() { my $interface; my $chainref; my $level; my $target; my $rule; my $list; my $chain; my $rejectref = new_standard_chain 'reject'; $level = $env{BLACKLIST_LOG_LEVEL} || 'info'; add_rule_pair new_standard_chain( 'logdrop' ), ' ' , 'DROP' , $level ; add_rule_pair new_standard_chain( 'logreject' ), ' ' , 'REJECT' , $level ; 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 ); } 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; } 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 = ) { 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 = ) { 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 ) , PREROUTE_RESTRICT , $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) , PREROUTE_RESTRICT , $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 ) , NO_RESTRICT , $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 = ) { 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' ); for my $interface ( keys %$source_ref ) { my $arrayref = $source_ref->{$interface}; 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}; } } if ( $config{DYNAMIC_ZONES} ) { push @rule_chains , [ $firewall_zone , $zone , $chain1 ]; push @rule_chains , [ $zone , $firewall_zone , $chain2 ]; } # # Take care of PREROUTING, INPUT and OUTPUT jumps # for my $typeref ( values %$source_hosts_ref ) { for my $interface (keys %$typeref ) { my $arrayref = $typeref->{$interface}; 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}{in_out}{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; push @rule_chains, [ $zone , $zone1 , $chain ] if $config{DYNAMIC_ZONES}; 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}{in_out}{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 ) { for my $interface ( keys %$typeref ) { my $arrayref = $typeref->{$interface}; 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 ) { for my $interface ( keys %$typeref ) { my $arrayref = $typeref->{$interface}; 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 ) { for my $interface1 ( keys %$type1ref ) { my $array1ref = $type1ref->{$interface1}; 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 ) { for my $interface ( keys %$typeref ) { my $arrayref = $typeref->{$interface}; 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'; } } } } sub setup_mss( $ ) { my $clampmss = $_[0]; my $option = "\Lclampmss" eq 'yes' ? '--clamp-mss-to-pmtu' : '--set-mss $clampmss'; add_rule $filter_table->{FORWARD} , "-p tcp --tcp-flags SYN,RST SYN -j TCPMSS $option"; } sub dump_rule_chains() { for my $arrayref ( @rule_chains ) { emit_unindented "@$arrayref"; } } 1;